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 +image + +## 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 userLoginAppleCallback(@RequestParam("code") String code, +// @RequestParam("id_token") String idToken) throws URISyntaxException { +// // Deep link 생성 +// String callback = String.format("intent://callback?code=%s&id_token=%s#Intent;package=%s;scheme=%s;end", +// code, idToken, androidPackage, androidScheme); +// +// // 리다이렉트 +// HttpHeaders httpHeaders = new HttpHeaders(); +// httpHeaders.setLocation(new URI(callback)); +// return new ResponseEntity<>(httpHeaders, HttpStatus.TEMPORARY_REDIRECT); +// } +//} diff --git a/src/main/java/com/moing/backend/domain/auth/presentation/constant/AuthResponseMessage.java b/src/main/java/com/moing/backend/domain/auth/presentation/constant/AuthResponseMessage.java new file mode 100644 index 00000000..e86f5204 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/auth/presentation/constant/AuthResponseMessage.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.auth.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AuthResponseMessage { + SIGN_IN_SUCCESS("로그인을 했습니다"), + SIGN_UP_SUCCESS("회원 가입을 했습니다"), + REISSUE_TOKEN_SUCCESS("토큰을 재발급했습니다"), + CHECK_NICKNAME_SUCCESS("닉네임 중복검사를 했습니다"); + private final String message; +} + diff --git a/src/main/java/com/moing/backend/domain/block/application/mapper/BlockMapper.java b/src/main/java/com/moing/backend/domain/block/application/mapper/BlockMapper.java new file mode 100644 index 00000000..a9c1e42d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/application/mapper/BlockMapper.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.block.application.mapper; + +import com.moing.backend.domain.block.domain.entity.Block; +import com.moing.backend.global.annotation.Mapper; + +@Mapper +public class BlockMapper { + + public static Block mapToBlock(Long memberId, Long targetId) { + return Block.builder() + .blockMemberId(memberId) + .targetId(targetId) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/block/application/service/BlockCreateUseCase.java b/src/main/java/com/moing/backend/domain/block/application/service/BlockCreateUseCase.java new file mode 100644 index 00000000..db8e6b25 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/application/service/BlockCreateUseCase.java @@ -0,0 +1,36 @@ +package com.moing.backend.domain.block.application.service; + +import com.moing.backend.domain.block.application.mapper.BlockMapper; +import com.moing.backend.domain.block.domain.service.BlockDeleteService; +import com.moing.backend.domain.block.domain.service.BlockQueryService; +import com.moing.backend.domain.block.domain.service.BlockSaveService; +import com.moing.backend.domain.board.domain.service.BoardGetService; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BlockCreateUseCase { + + private final MemberGetService memberGetService; + private final BlockSaveService blockSaveService; + + + /** + * 차단 하기 + */ + + public Long createBlock(String socialId, Long targetId) { + Long memberId = memberGetService.getMemberBySocialId(socialId).getMemberId(); + + blockSaveService.save(BlockMapper.mapToBlock(memberId, targetId)); + + return targetId; + } + + +} diff --git a/src/main/java/com/moing/backend/domain/block/application/service/BlockDeleteUseCase.java b/src/main/java/com/moing/backend/domain/block/application/service/BlockDeleteUseCase.java new file mode 100644 index 00000000..1101ccc8 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/application/service/BlockDeleteUseCase.java @@ -0,0 +1,34 @@ +package com.moing.backend.domain.block.application.service; + +import com.moing.backend.domain.block.application.mapper.BlockMapper; +import com.moing.backend.domain.block.domain.service.BlockDeleteService; +import com.moing.backend.domain.block.domain.service.BlockQueryService; +import com.moing.backend.domain.block.domain.service.BlockSaveService; +import com.moing.backend.domain.board.domain.service.BoardGetService; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BlockDeleteUseCase { + + private final MemberGetService memberGetService; + private final BlockDeleteService blockDeleteService; + + + /** + * 차단 철회하기 + */ + + public Long deleteBlock(String socialId, Long targetId) { + Long memberId = memberGetService.getMemberBySocialId(socialId).getMemberId(); + blockDeleteService.delete(memberId, targetId); + + return targetId; + } + +} diff --git a/src/main/java/com/moing/backend/domain/block/application/service/BlockReadUseCase.java b/src/main/java/com/moing/backend/domain/block/application/service/BlockReadUseCase.java new file mode 100644 index 00000000..6f108b08 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/application/service/BlockReadUseCase.java @@ -0,0 +1,32 @@ +package com.moing.backend.domain.block.application.service; + +import com.moing.backend.domain.block.domain.service.BlockQueryService; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.report.application.dto.BlockMemberRes; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BlockReadUseCase { + + private final MemberGetService memberGetService; + private final BlockQueryService blockQueryService; + + /** + * 차단한 리스트 조회하기 + */ + public List getMyBlockList(String socialId) { + Long memberId = memberGetService.getMemberBySocialId(socialId).getMemberId(); + + return blockQueryService.getBlockLists(memberId); + } + public List getMyBlockInfoList(String socialId) { + Long memberId = memberGetService.getMemberBySocialId(socialId).getMemberId(); + + return blockQueryService.getBlockInfoLists(memberId); + } + +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/entity/Block.java b/src/main/java/com/moing/backend/domain/block/domain/entity/Block.java new file mode 100644 index 00000000..2f2f22a7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/entity/Block.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.block.domain.entity; + +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Builder +public class Block extends BaseTimeEntity { + + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "block_id") + private Long id; + + private Long blockMemberId; + private Long targetId; + + public Block(Long blockMemberId, Long targetId) { + this.blockMemberId=blockMemberId; + this.targetId=targetId; + } +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/repository/BlockCustomRepository.java b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockCustomRepository.java new file mode 100644 index 00000000..8dd0685e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockCustomRepository.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.block.domain.repository; + +import com.moing.backend.domain.block.domain.entity.Block; +import com.moing.backend.domain.report.application.dto.BlockMemberRes; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + + +public interface BlockCustomRepository{ + + Optional> getMyBlockList(Long memberId); + Optional> getMyBlockInfoList(Long memberId); + Optional getBlockById(Long memberId, Long targetId); + + } diff --git a/src/main/java/com/moing/backend/domain/block/domain/repository/BlockCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockCustomRepositoryImpl.java new file mode 100644 index 00000000..1856d27a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockCustomRepositoryImpl.java @@ -0,0 +1,61 @@ +package com.moing.backend.domain.block.domain.repository; + +import com.moing.backend.domain.block.domain.entity.Block; +import com.moing.backend.domain.block.domain.entity.QBlock; +import com.moing.backend.domain.member.domain.entity.QMember; +import com.moing.backend.domain.report.application.dto.BlockMemberRes; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import javax.persistence.EntityManager; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.block.domain.entity.QBlock.block; +import static com.moing.backend.domain.member.domain.entity.QMember.member; + +public class BlockCustomRepositoryImpl implements BlockCustomRepository { + + private final JPAQueryFactory queryFactory; + + public BlockCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + + @Override + public Optional> getMyBlockList(Long memberId) { + return Optional.ofNullable(queryFactory + .select(block.targetId) + .from(block) + .where(block.blockMemberId.eq(memberId)) + .fetch()); + } + @Override + public Optional> getMyBlockInfoList(Long memberId) { + return Optional.ofNullable(queryFactory + .select(Projections.constructor(BlockMemberRes.class, + block.targetId, + member.nickName, + member.introduction, + member.profileImage + )) + .from(block) + .join(member) + .on(member.memberId.eq(block.targetId)) + .where(block.blockMemberId.eq(memberId)) + .fetch()); + } + + @Override + public Optional getBlockById(Long memberId, Long targetId) { + return Optional.ofNullable(queryFactory + .selectFrom(block) + .where( + block.blockMemberId.eq(memberId), + block.targetId.eq(targetId)).fetchFirst()); + + } +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/repository/BlockRepository.java b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockRepository.java new file mode 100644 index 00000000..8de0870a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockRepository.java @@ -0,0 +1,8 @@ +package com.moing.backend.domain.block.domain.repository; + +import com.moing.backend.domain.block.domain.entity.Block; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BlockRepository extends JpaRepository,BlockCustomRepository { + +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/repository/BlockRepositoryUtils.java b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockRepositoryUtils.java new file mode 100644 index 00000000..a3ccffd1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/repository/BlockRepositoryUtils.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.block.domain.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.JPAExpressions; + +import static com.moing.backend.domain.block.domain.entity.QBlock.block; + +public class BlockRepositoryUtils { + public static BooleanExpression blockCondition(Long memberId, NumberPath targetMemberId) { + return JPAExpressions + .select(block.id) + .from(block) + .where(block.blockMemberId.eq(memberId), + block.targetId.eq(targetMemberId)) + .notExists(); + } + + public static BooleanExpression blockCondition(NumberPath memberId, Long targetMemberId) { + return JPAExpressions + .select(block.id) + .from(block) + .where(block.blockMemberId.eq(memberId), + block.targetId.eq(targetMemberId)) + .notExists(); + } +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/service/BlockDeleteService.java b/src/main/java/com/moing/backend/domain/block/domain/service/BlockDeleteService.java new file mode 100644 index 00000000..0c23769d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/service/BlockDeleteService.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.block.domain.service; + +import com.moing.backend.domain.block.domain.entity.Block; +import com.moing.backend.domain.block.domain.repository.BlockRepository; +import com.moing.backend.domain.block.exception.NotFoundBlockException; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class BlockDeleteService { + private final BlockRepository blockRepository; + + public void delete(Long memberId, Long targetId) { + Block block = blockRepository.getBlockById(memberId, targetId).orElseThrow(NotFoundBlockException::new); + blockRepository.delete(block); + } +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/service/BlockQueryService.java b/src/main/java/com/moing/backend/domain/block/domain/service/BlockQueryService.java new file mode 100644 index 00000000..8614a9a9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/service/BlockQueryService.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.block.domain.service; + +import com.moing.backend.domain.block.domain.entity.Block; +import com.moing.backend.domain.block.domain.repository.BlockRepository; +import com.moing.backend.domain.block.exception.NotFoundBlockException; +import com.moing.backend.domain.report.application.dto.BlockMemberRes; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@DomainService +@RequiredArgsConstructor +public class BlockQueryService { + + private final BlockRepository blockRepository; + + public List getBlockLists(Long memberId) { + return blockRepository.getMyBlockList(memberId).orElseThrow(NotFoundBlockException::new); + } + public List getBlockInfoLists(Long memberId) { + return blockRepository.getMyBlockInfoList(memberId).orElseThrow(NotFoundBlockException::new); + } + + public Block getBlock(Long memberId, Long targetId) { + return blockRepository.getBlockById(memberId, targetId).orElseThrow(NotFoundBlockException::new); + } +} diff --git a/src/main/java/com/moing/backend/domain/block/domain/service/BlockSaveService.java b/src/main/java/com/moing/backend/domain/block/domain/service/BlockSaveService.java new file mode 100644 index 00000000..c79a8920 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/domain/service/BlockSaveService.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.block.domain.service; + +import com.moing.backend.domain.block.domain.entity.Block; +import com.moing.backend.domain.block.domain.repository.BlockRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; + +@DomainService +@RequiredArgsConstructor +public class BlockSaveService { + + private final BlockRepository blockRepository; + + + public Block save(Block block) { + //기존에 동일한 사람이 차단했으면 중복 제거 + Optional findBlock=blockRepository.getBlockById(block.getBlockMemberId(), block.getTargetId()); + return findBlock.orElseGet(() -> blockRepository.save(block)); + } +} diff --git a/src/main/java/com/moing/backend/domain/block/exception/BlockException.java b/src/main/java/com/moing/backend/domain/block/exception/BlockException.java new file mode 100644 index 00000000..31375d3b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/exception/BlockException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.block.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class BlockException extends ApplicationException { + protected BlockException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/block/exception/NotFoundBlockException.java b/src/main/java/com/moing/backend/domain/block/exception/NotFoundBlockException.java new file mode 100644 index 00000000..2d15c0d7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/exception/NotFoundBlockException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.block.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundBlockException extends BlockException { + public NotFoundBlockException() { + super(ErrorCode.NOT_FOUND_BY_BOARD_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/block/presentation/BlockController.java b/src/main/java/com/moing/backend/domain/block/presentation/BlockController.java new file mode 100644 index 00000000..4d999ef4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/presentation/BlockController.java @@ -0,0 +1,51 @@ +package com.moing.backend.domain.block.presentation; + +import com.moing.backend.domain.block.application.service.BlockCreateUseCase; +import com.moing.backend.domain.block.application.service.BlockDeleteUseCase; +import com.moing.backend.domain.block.application.service.BlockReadUseCase; +import com.moing.backend.domain.block.domain.service.BlockDeleteService; +import com.moing.backend.domain.report.application.dto.BlockMemberRes; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.moing.backend.domain.block.presentation.constant.BlockResponseMessage.*; +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.CREATE_REPORT_SUCCESS; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/block") +public class BlockController { + + private final BlockReadUseCase blockReadUseCase; + private final BlockCreateUseCase blockCreateUseCase; + private final BlockDeleteUseCase blockDeleteUseCase; + + @PostMapping("/{targetId}") + public ResponseEntity> createBlock(@AuthenticationPrincipal User user, + @PathVariable("targetId") Long targetId) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_BLOCK_SUCCESS.getMessage(), this.blockCreateUseCase.createBlock(user.getSocialId(), targetId))); + } + + @DeleteMapping("/{targetId}") + public ResponseEntity> deleteBlock(@AuthenticationPrincipal User user, + @PathVariable("targetId") Long targetId) { + return ResponseEntity.ok(SuccessResponse.create(DELETE_BLOCK_SUCCESS.getMessage(), this.blockDeleteUseCase.deleteBlock(user.getSocialId(), targetId))); + } + + @GetMapping("") + public ResponseEntity>> getBlocks(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_BLOCK_SUCCESS.getMessage(), this.blockReadUseCase.getMyBlockList(user.getSocialId()))); + } + + @GetMapping("/info") + public ResponseEntity>> getBlockList(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_BLOCK_SUCCESS.getMessage(), this.blockReadUseCase.getMyBlockInfoList(user.getSocialId()))); + } + +} diff --git a/src/main/java/com/moing/backend/domain/block/presentation/constant/BlockResponseMessage.java b/src/main/java/com/moing/backend/domain/block/presentation/constant/BlockResponseMessage.java new file mode 100644 index 00000000..ab3e5734 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/block/presentation/constant/BlockResponseMessage.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.block.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BlockResponseMessage { + + CREATE_BLOCK_SUCCESS("사용자 차단을 완료했습니다."), + GET_BLOCK_SUCCESS("사용자 차단 목록 조회를 완료했습니다."), + DELETE_BLOCK_SUCCESS("사용자 차단 해제를 완료했습니다."); + + private final String message; + +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/request/CreateBoardRequest.java b/src/main/java/com/moing/backend/domain/board/application/dto/request/CreateBoardRequest.java new file mode 100644 index 00000000..b639ee3c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/request/CreateBoardRequest.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.board.application.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class CreateBoardRequest { + @NotBlank(message = "title 을 입력해 주세요.") + @Size(min = 0, max = 30, message = "제목 글자수를 초과했습니다.") + private String title; + + @NotBlank(message = "content 을 입력해 주세요.") + @Size(min = 0, max = 300, message = "내용 글자수를 초과했습니다.") + private String content; + + @NotNull(message = "notice 사용 여부(isNotice) 를 입력해 주세요.") + private Boolean isNotice; +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/request/UpdateBoardRequest.java b/src/main/java/com/moing/backend/domain/board/application/dto/request/UpdateBoardRequest.java new file mode 100644 index 00000000..685ee2ba --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/request/UpdateBoardRequest.java @@ -0,0 +1,23 @@ +package com.moing.backend.domain.board.application.dto.request; + +import lombok.Builder; +import lombok.Getter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Getter +@Builder +public class UpdateBoardRequest { + @NotBlank(message = "title 을 입력해 주세요.") + @Size(min = 1, max = 15, message = "title 은 최소 1개, 최대 15개의 문자만 입력 가능합니다.") + private String title; + + @NotBlank(message = "content 을 입력해 주세요.") + @Size(min = 1, max = 300, message = "content 은 최소 1개, 최대 10개의 문자만 입력 가능합니다.") + private String content; + + @NotNull(message = "notice 사용 여부(isNotice) 를 입력해 주세요.") + private Boolean isNotice; +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/response/BoardBlocks.java b/src/main/java/com/moing/backend/domain/board/application/dto/response/BoardBlocks.java new file mode 100644 index 00000000..6deb3f17 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/response/BoardBlocks.java @@ -0,0 +1,65 @@ +package com.moing.backend.domain.board.application.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BoardBlocks { + + private Long boardId; + + private String writerNickName; + + private Boolean writerIsLeader; + + private String writerProfileImage; + + private String title; + + private String content; + + private Integer commentNum; + + private Boolean isRead; + + private Boolean writerIsDeleted; + + private boolean isNotice; + + private Long makerId; + + + @QueryProjection + public BoardBlocks(Long boardId, String writerNickName, Boolean writerIsLeader, String writerProfileImage, String title, String content, Integer commentNum, Boolean isRead, Boolean writerIsDeleted, boolean isNotice, Long makerId) { + this.boardId = boardId; + this.writerNickName = writerNickName; + this.writerIsLeader = writerIsLeader; + this.writerProfileImage = writerProfileImage; + this.title = title; + this.content = content; + this.commentNum = commentNum; + this.isRead = isRead; + this.writerIsDeleted=writerIsDeleted; + this.isNotice=isNotice; + this.makerId = makerId; + deleteMember(); + } + + public void readBoard() { + this.isRead = true; + } + + public boolean isNotice() { + return isNotice; + } + + public void deleteMember() { + if(Boolean.TRUE.equals(writerIsDeleted)) { + this.writerNickName = "(알 수 없음)"; + this.writerProfileImage = null; + } + } +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/response/CreateBoardResponse.java b/src/main/java/com/moing/backend/domain/board/application/dto/response/CreateBoardResponse.java new file mode 100644 index 00000000..75662b71 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/response/CreateBoardResponse.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.board.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class CreateBoardResponse { + private Long boardId; +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/response/GetAllBoardResponse.java b/src/main/java/com/moing/backend/domain/board/application/dto/response/GetAllBoardResponse.java new file mode 100644 index 00000000..f020d9e7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/response/GetAllBoardResponse.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.board.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class GetAllBoardResponse { + private int noticeNum; + private List noticeBlocks=new ArrayList<>(); + private int notNoticeNum; + private List notNoticeBlocks=new ArrayList<>(); +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/response/GetBoardDetailResponse.java b/src/main/java/com/moing/backend/domain/board/application/dto/response/GetBoardDetailResponse.java new file mode 100644 index 00000000..92ac6ad7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/response/GetBoardDetailResponse.java @@ -0,0 +1,33 @@ +package com.moing.backend.domain.board.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class GetBoardDetailResponse { + + private Long boardId; + + private String writerNickName; + + private Boolean writerIsLeader; + + private String writerProfileImage; + + private String title; + + private String content; + + private String createdDate; + + private Boolean isWriter; + + private Boolean isNotice; + + private Long makerId; +} diff --git a/src/main/java/com/moing/backend/domain/board/application/dto/response/UpdateBoardResponse.java b/src/main/java/com/moing/backend/domain/board/application/dto/response/UpdateBoardResponse.java new file mode 100644 index 00000000..48b4b114 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/dto/response/UpdateBoardResponse.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.board.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class UpdateBoardResponse { + private Long boardId; +} diff --git a/src/main/java/com/moing/backend/domain/board/application/mapper/BoardMapper.java b/src/main/java/com/moing/backend/domain/board/application/mapper/BoardMapper.java new file mode 100644 index 00000000..eed680d5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/mapper/BoardMapper.java @@ -0,0 +1,57 @@ +package com.moing.backend.domain.board.application.mapper; + +import com.moing.backend.domain.board.application.dto.request.CreateBoardRequest; +import com.moing.backend.domain.board.application.dto.response.GetBoardDetailResponse; +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +@Component +@RequiredArgsConstructor +public class BoardMapper { + + public static Board toBoard(TeamMember teamMember, Team team, CreateBoardRequest createBoardRequest, boolean isLeader) { + Board board = Board.builder() + .title(createBoardRequest.getTitle()) + .content(createBoardRequest.getContent()) + .isNotice(createBoardRequest.getIsNotice()) + .commentNum(0) + .isLeader(isLeader) + .boardReads(new ArrayList<>()) + .boardComments(new ArrayList<>()) + .build(); + board.updateTeamMember(teamMember); + board.updateTeam(team); + return board; + } + + public static GetBoardDetailResponse toBoardDetail(Board board, boolean isWriter, boolean writerIsDeleted) { + String nickName = writerIsDeleted ? "(알 수 없음)" : board.getTeamMember().getMember().getNickName(); + String writerProfileImage = writerIsDeleted ? null : board.getTeamMember().getMember().getProfileImage(); + Long writerId = writerIsDeleted ? 0L : board.getTeamMember().getMember().getMemberId(); + return GetBoardDetailResponse.builder() + .boardId(board.getBoardId()) + .title(board.getTitle()) + .content(board.getContent()) + .writerNickName(nickName) + .writerIsLeader(board.isLeader()) + .writerProfileImage(writerProfileImage) + .createdDate(getFormattedDate(board.getCreatedDate())) + .isWriter(isWriter) + .isNotice(board.isNotice()) + .makerId(writerId) + .build(); + } + + public static String getFormattedDate(LocalDateTime localDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + return localDateTime.format(formatter); + } + +} diff --git a/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUseCase.java b/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUseCase.java new file mode 100644 index 00000000..c7b69d31 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/service/CreateBoardUseCase.java @@ -0,0 +1,51 @@ +package com.moing.backend.domain.board.application.service; + +import com.moing.backend.domain.board.application.dto.request.CreateBoardRequest; +import com.moing.backend.domain.board.application.dto.response.CreateBoardResponse; +import com.moing.backend.domain.board.application.mapper.BoardMapper; +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.board.domain.service.BoardSaveService; +import com.moing.backend.domain.boardRead.application.service.CreateBoardReadUseCase; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.history.domain.service.AlarmHistorySaveService; +import com.moing.backend.domain.team.application.service.CheckLeaderUseCase; +import com.moing.backend.global.response.BaseServiceResponse; +import com.moing.backend.global.utils.BaseService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class CreateBoardUseCase { + + + private final BoardSaveService boardSaveService; + private final CheckLeaderUseCase checkLeaderUseCase; + private final CreateBoardReadUseCase createBoardReadUseCase; + private final BaseService baseService; + private final SendBoardAlarmUseCase sendBoardAlarmUseCase; + + /** + * 게시글 생성 + */ + public CreateBoardResponse createBoard(String socialId, Long teamId, CreateBoardRequest createBoardRequest) { + //1, 게시글 생성, 저장 + BaseServiceResponse data=baseService.getCommonData(socialId, teamId); + boolean isLeader = checkLeaderUseCase.isTeamLeader(data.getMember(), data.getTeam()); //작성자 리더 여부 + Board board=boardSaveService.saveBoard(BoardMapper.toBoard(data.getTeamMember(), data.getTeam(), createBoardRequest, isLeader)); + + //2. 읽음 처리 - 생성한 사람은 무조건 읽음 + createBoardReadUseCase.createBoardRead(data.getTeam(), data.getMember(), board); + + //3. 알림 보내기 - 공지인 경우 + sendBoardAlarmUseCase.sendNewUploadAlarm(data, board); + + return new CreateBoardResponse(board.getBoardId()); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUseCase.java b/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUseCase.java new file mode 100644 index 00000000..f601362f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/service/DeleteBoardUseCase.java @@ -0,0 +1,32 @@ +package com.moing.backend.domain.board.application.service; + +import com.moing.backend.domain.board.domain.service.BoardDeleteService; +import com.moing.backend.domain.board.exception.NotAuthByBoardException; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import com.moing.backend.global.utils.BaseBoardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class DeleteBoardUseCase { + + private final BaseBoardService baseBoardService; + private final BoardDeleteService boardDeleteService; + + /** + * 게시글 삭제 + */ + public void deleteBoard(String socialId, Long teamId, Long boardId){ + //1. 게시글 조회 + BaseBoardServiceResponse data= baseBoardService.getCommonData(socialId,teamId,boardId); + //2. 작성자인 경우 + if (data.getTeamMember() == data.getBoard().getTeamMember()) { + //3. 삭제 + boardDeleteService.deleteBoard(data.getBoard()); + } else throw new NotAuthByBoardException(); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUseCase.java b/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUseCase.java new file mode 100644 index 00000000..5ac5e4d9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/service/GetBoardUseCase.java @@ -0,0 +1,47 @@ +package com.moing.backend.domain.board.application.service; + +import com.moing.backend.domain.board.application.dto.response.GetAllBoardResponse; +import com.moing.backend.domain.board.application.dto.response.GetBoardDetailResponse; +import com.moing.backend.domain.board.application.mapper.BoardMapper; +import com.moing.backend.domain.board.domain.service.BoardGetService; +import com.moing.backend.domain.boardRead.application.service.CreateBoardReadUseCase; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import com.moing.backend.global.utils.BaseBoardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class GetBoardUseCase { + + + private final BaseBoardService baseBoardService; + private final MemberGetService memberGetService; + private final CreateBoardReadUseCase createBoardReadUseCase; + private final BoardGetService boardGetService; + + + /** + * 게시글 상세 조회 + */ + @Transactional + public GetBoardDetailResponse getBoardDetail(String socialId, Long teamId, Long boardId) { + // 1. 게시글 조회 + BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); + // 2. 읽음 처리 + createBoardReadUseCase.createBoardRead(data.getTeam(), data.getMember(), data.getBoard()); + return BoardMapper.toBoardDetail(data.getBoard(), data.getTeamMember() == data.getBoard().getTeamMember(), data.getBoard().getTeamMember().isDeleted()); + } + + /** + * 게시글 전체 조회 + */ + @Transactional(readOnly = true) + public GetAllBoardResponse getAllBoard(String socialId, Long teamId){ + Member member=memberGetService.getMemberBySocialId(socialId); + return boardGetService.getBoardAll(teamId, member.getMemberId()); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/application/service/SendBoardAlarmUseCase.java b/src/main/java/com/moing/backend/domain/board/application/service/SendBoardAlarmUseCase.java new file mode 100644 index 00000000..edb57a8c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/service/SendBoardAlarmUseCase.java @@ -0,0 +1,57 @@ +package com.moing.backend.domain.board.application.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent; +import com.moing.backend.global.response.BaseServiceResponse; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.history.domain.entity.AlarmType.NEW_UPLOAD; +import static com.moing.backend.global.config.fcm.constant.NewNoticeUploadMessage.NEW_NOTICE_UPLOAD_MESSAGE; + +@Service +@RequiredArgsConstructor +@Transactional +public class SendBoardAlarmUseCase { + + private final TeamMemberGetService teamMemberGetService; + private final ApplicationEventPublisher eventPublisher; + + public void sendNewUploadAlarm(BaseServiceResponse baseServiceResponse, Board board) { + Member member = baseServiceResponse.getMember(); + Team team = baseServiceResponse.getTeam(); + + if (board.isNotice()) { + String title = NEW_NOTICE_UPLOAD_MESSAGE.title(team.getName()); + String body = NEW_NOTICE_UPLOAD_MESSAGE.body(board.getTitle()); + Optional> newUploadInfos=teamMemberGetService.getNewUploadInfo(team.getTeamId(), member.getMemberId()); + Optional> memberIdAndTokensByPush = AlarmHistoryMapper.getNewUploadPushInfo(newUploadInfos); + Optional> memberIdAndTokensBySave = AlarmHistoryMapper.getNewUploadSaveInfo(newUploadInfos); + // 알림 보내기 + eventPublisher.publishEvent(new MultiFcmEvent(title, body, memberIdAndTokensByPush, memberIdAndTokensBySave, createIdInfo(team.getTeamId(), board.getBoardId()), team.getName(), NEW_UPLOAD, PagePath.NOTICE_PATH.getValue())); + } + } + + private String createIdInfo(Long teamId, Long boardId) { + JSONObject jo = new JSONObject(); + jo.put("teamId", teamId); + jo.put("boardId", boardId); + jo.put("type", "NEW_UPLOAD_BOARD"); + return jo.toJSONString(); + } + +} + diff --git a/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUseCase.java b/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUseCase.java new file mode 100644 index 00000000..d95582e3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/application/service/UpdateBoardUseCase.java @@ -0,0 +1,33 @@ +package com.moing.backend.domain.board.application.service; + +import com.moing.backend.domain.board.application.dto.request.UpdateBoardRequest; +import com.moing.backend.domain.board.application.dto.response.UpdateBoardResponse; +import com.moing.backend.domain.board.exception.NotAuthByBoardException; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import com.moing.backend.global.utils.BaseBoardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class UpdateBoardUseCase { + + private final BaseBoardService baseBoardService; + + /** + * 게시글 수정 + */ + public UpdateBoardResponse updateBoard(String socialId, Long teamId, Long boardId, UpdateBoardRequest updateBoardRequest){ + // 1. 게시글 조회 + BaseBoardServiceResponse data= baseBoardService.getCommonData(socialId, teamId, boardId); + // 2. 게시글 작성자만 + if (data.getTeamMember() == data.getBoard().getTeamMember()) { + // 3. 수정 + data.getBoard().updateBoard(updateBoardRequest); + return new UpdateBoardResponse(data.getBoard().getBoardId()); + } else throw new NotAuthByBoardException(); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/entity/Board.java b/src/main/java/com/moing/backend/domain/board/domain/entity/Board.java new file mode 100644 index 00000000..dc2b5899 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/entity/Board.java @@ -0,0 +1,86 @@ +package com.moing.backend.domain.board.domain.entity; + +import com.moing.backend.domain.board.application.dto.request.UpdateBoardRequest; +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardRead.domain.entity.BoardRead; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class Board extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "board_id") + private Long boardId; + + private boolean isLeader; /*작성자 소모임장유무*/ + + + @Column(nullable = false, length = 30) + private String title; + + @Column(nullable = false, length = 300) + private String content; + + private boolean isNotice; + + //반정규화 -> 댓글 개수 + private Integer commentNum; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_member_id") + private TeamMember teamMember; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id") + private Team team; + + @OneToMany(mappedBy = "board", cascade = CascadeType.ALL) + private List boardReads = new ArrayList<>(); + + @BatchSize(size=10) + @OneToMany(mappedBy = "board", cascade = CascadeType.ALL) + private List boardComments = new ArrayList<>(); + + public void incrComNum() { + this.commentNum++; + } + + public void decrComNum() { + this.commentNum--; + } + + public void updateBoard(UpdateBoardRequest updateBoardRequest){ + this.title= updateBoardRequest.getTitle(); + this.content= updateBoardRequest.getContent(); + this.isNotice= updateBoardRequest.getIsNotice(); + } + + //==연관관계 메서드 ==// + public void updateTeamMember(TeamMember teamMember) { + this.teamMember = teamMember; + } + + public void updateTeam(Team team) { + this.team = team; + } + + public String getWriterNickName() { + return teamMember.getMemberNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/repository/BoardCustomRepository.java b/src/main/java/com/moing/backend/domain/board/domain/repository/BoardCustomRepository.java new file mode 100644 index 00000000..ec8f67d9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/repository/BoardCustomRepository.java @@ -0,0 +1,8 @@ +package com.moing.backend.domain.board.domain.repository; + +import com.moing.backend.domain.board.application.dto.response.GetAllBoardResponse; + +public interface BoardCustomRepository { + GetAllBoardResponse findBoardAll(Long teamId, Long memberId); + Integer findUnReadBoardNum(Long teamId, Long memberId); +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/repository/BoardCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/board/domain/repository/BoardCustomRepositoryImpl.java new file mode 100644 index 00000000..c0b9c927 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/repository/BoardCustomRepositoryImpl.java @@ -0,0 +1,97 @@ +package com.moing.backend.domain.board.domain.repository; + +import com.moing.backend.domain.block.domain.repository.BlockRepositoryUtils; +import com.moing.backend.domain.board.application.dto.response.BoardBlocks; +import com.moing.backend.domain.board.application.dto.response.GetAllBoardResponse; +import com.moing.backend.domain.board.application.dto.response.QBoardBlocks; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.util.ArrayList; +import java.util.List; + +import static com.moing.backend.domain.board.domain.entity.QBoard.board; +import static com.moing.backend.domain.boardRead.domain.entity.QBoardRead.boardRead; +import static com.moing.backend.domain.member.domain.entity.QMember.member; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; + +public class BoardCustomRepositoryImpl implements BoardCustomRepository { + + private final JPAQueryFactory queryFactory; + public BoardCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + @Override + public GetAllBoardResponse findBoardAll(Long teamId, Long memberId) { + + + BooleanExpression blockCondition = BlockRepositoryUtils.blockCondition(memberId, board.teamMember.member.memberId); + + List allBoardBlocks = queryFactory + .select(new QBoardBlocks( + board.boardId, + board.teamMember.member.nickName.coalesce("알수없음"), + board.isLeader, + board.teamMember.member.profileImage, + board.title, + board.content, + board.commentNum, + isReadExpression(memberId, teamId).as("isRead"), + board.teamMember.isDeleted, + board.isNotice, + board.teamMember.member.memberId)) + .from(board) + .leftJoin(board.teamMember, teamMember) + .leftJoin(board.teamMember.member, member) + .where(board.team.teamId.eq(teamId) + .and(blockCondition)) + .orderBy(board.createdDate.desc()) + .fetch(); + + // 공지와 일반 게시글로 나누기 및 읽음 여부 적용 + List noticeBlocks = new ArrayList<>(); + List regularBlocks = new ArrayList<>(); + allBoardBlocks.forEach(block -> { + if (block.isNotice()) { + noticeBlocks.add(block); + } else { + regularBlocks.add(block); + } + }); + + return new GetAllBoardResponse(noticeBlocks.size(), noticeBlocks, regularBlocks.size(), regularBlocks); + } + + @Override + public Integer findUnReadBoardNum(Long teamId, Long memberId) { + BooleanExpression isNotReadExpression = isReadExpression(memberId, teamId).not(); + + BooleanExpression blockCondition = BlockRepositoryUtils.blockCondition(memberId, board.teamMember.member.memberId); + + Long unReadBoardsCount = queryFactory + .select(board.count()) + .from(board) + .leftJoin(boardRead) + .on(board.boardId.eq(boardRead.board.boardId) + .and(boardRead.member.memberId.eq(memberId))) + .where(board.team.teamId.eq(teamId) + .and(isNotReadExpression) + .and(blockCondition)) + .fetchOne(); + + return Math.toIntExact(unReadBoardsCount != null ? unReadBoardsCount : 0); + } + + + private BooleanExpression isReadExpression(Long memberId, Long teamId) { + return JPAExpressions + .select(boardRead.board.boardId) + .from(boardRead) + .where(boardRead.member.memberId.eq(memberId), + boardRead.board.team.teamId.eq(teamId), + boardRead.board.boardId.eq(board.boardId)) + .exists(); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/repository/BoardRepository.java b/src/main/java/com/moing/backend/domain/board/domain/repository/BoardRepository.java new file mode 100644 index 00000000..4059cec3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/repository/BoardRepository.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.board.domain.repository; + +import com.moing.backend.domain.board.domain.entity.Board; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BoardRepository extends JpaRepository, BoardCustomRepository { + + Optional findBoardByBoardId(Long boardId); +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/service/BoardDeleteService.java b/src/main/java/com/moing/backend/domain/board/domain/service/BoardDeleteService.java new file mode 100644 index 00000000..cb016b83 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/service/BoardDeleteService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.board.domain.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.board.domain.repository.BoardRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class BoardDeleteService { + + private final BoardRepository boardRepository; + + public void deleteBoard(Board board){ + boardRepository.delete(board); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/service/BoardGetService.java b/src/main/java/com/moing/backend/domain/board/domain/service/BoardGetService.java new file mode 100644 index 00000000..2a20429d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/service/BoardGetService.java @@ -0,0 +1,32 @@ +package com.moing.backend.domain.board.domain.service; + +import com.moing.backend.domain.board.application.dto.response.GetAllBoardResponse; +import com.moing.backend.domain.board.application.dto.response.GetBoardDetailResponse; +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.board.domain.repository.BoardRepository; +import com.moing.backend.domain.board.exception.NotFoundByBoardIdException; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.Optional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class BoardGetService { + + private final BoardRepository boardRepository; + + public Board getBoard(Long boardId){ + return boardRepository.findBoardByBoardId(boardId).orElseThrow(NotFoundByBoardIdException::new); + } + + public GetAllBoardResponse getBoardAll(Long teamId, Long memberId){ + return boardRepository.findBoardAll(teamId, memberId); + } + + public Integer getUnReadBoardNum(Long teamId, Long memberId){ + return boardRepository.findUnReadBoardNum(teamId, memberId); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/domain/service/BoardSaveService.java b/src/main/java/com/moing/backend/domain/board/domain/service/BoardSaveService.java new file mode 100644 index 00000000..e2e800ff --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/domain/service/BoardSaveService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.board.domain.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.board.domain.repository.BoardRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class BoardSaveService { + + private final BoardRepository boardRepository; + + public Board saveBoard(Board board) { + return this.boardRepository.save(board); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/exception/BoardException.java b/src/main/java/com/moing/backend/domain/board/exception/BoardException.java new file mode 100644 index 00000000..d0502d4e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/exception/BoardException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.board.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class BoardException extends ApplicationException { + protected BoardException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/board/exception/NotAuthByBoardException.java b/src/main/java/com/moing/backend/domain/board/exception/NotAuthByBoardException.java new file mode 100644 index 00000000..704c0873 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/exception/NotAuthByBoardException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.board.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotAuthByBoardException extends BoardException { + public NotAuthByBoardException() { + super(ErrorCode.NOT_AUTH_BY_BOARD_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/exception/NotFoundByBoardIdException.java b/src/main/java/com/moing/backend/domain/board/exception/NotFoundByBoardIdException.java new file mode 100644 index 00000000..e9fbb6c7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/exception/NotFoundByBoardIdException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.board.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundByBoardIdException extends BoardException { + public NotFoundByBoardIdException() { + super(ErrorCode.NOT_FOUND_BY_BOARD_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/board/presentation/BoardController.java b/src/main/java/com/moing/backend/domain/board/presentation/BoardController.java new file mode 100644 index 00000000..6e0c89c4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/presentation/BoardController.java @@ -0,0 +1,95 @@ +package com.moing.backend.domain.board.presentation; + +import com.moing.backend.domain.board.application.dto.request.CreateBoardRequest; +import com.moing.backend.domain.board.application.dto.request.UpdateBoardRequest; +import com.moing.backend.domain.board.application.dto.response.CreateBoardResponse; +import com.moing.backend.domain.board.application.dto.response.GetAllBoardResponse; +import com.moing.backend.domain.board.application.dto.response.GetBoardDetailResponse; +import com.moing.backend.domain.board.application.dto.response.UpdateBoardResponse; +import com.moing.backend.domain.board.application.service.CreateBoardUseCase; +import com.moing.backend.domain.board.application.service.DeleteBoardUseCase; +import com.moing.backend.domain.board.application.service.GetBoardUseCase; +import com.moing.backend.domain.board.application.service.UpdateBoardUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +import static com.moing.backend.domain.board.presentation.constant.BoardResponseMessage.*; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/{teamId}/board") +public class BoardController { + + private final CreateBoardUseCase createBoardUseCase; + private final UpdateBoardUseCase updateBoardUseCase; + private final GetBoardUseCase getBoardUseCase; + private final DeleteBoardUseCase deleteBoardUseCase; + + /** + * 게시글 생성 + * [POST] api/{teamId}/board + * 작성자 : 김민수 + */ + @PostMapping + public ResponseEntity> createBoard(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @Valid @RequestBody CreateBoardRequest createBoardRequest) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_BOARD_SUCCESS.getMessage(), this.createBoardUseCase.createBoard(user.getSocialId(), teamId, createBoardRequest))); + } + + /** + * 게시글 수정 + * [PUT] api/{teamId}/board/{boardId} + * 작성자 : 김민수 + */ + @PutMapping("/{boardId}") + public ResponseEntity> updateBoard(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long boardId, + @Valid @RequestBody UpdateBoardRequest updateBoardRequest) { + return ResponseEntity.ok(SuccessResponse.create(UPDATE_BOARD_SUCCESS.getMessage(), this.updateBoardUseCase.updateBoard(user.getSocialId(), teamId, boardId, updateBoardRequest))); + } + + /** + * 게시글 삭제 + * [DELETE] api/{teamId}/board/{boardId} + * 작성자 : 김민수 + */ + @DeleteMapping("/{boardId}") + public ResponseEntity deleteBoard(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long boardId) { + this.deleteBoardUseCase.deleteBoard(user.getSocialId(), teamId, boardId); + return ResponseEntity.ok(SuccessResponse.create(DELETE_BOARD_SUCCESS.getMessage())); + } + + /** + * 게시글 상세 조회 + * [GET] api/{teamId}/board/{boardId} + * 작성자 : 김민수 + */ + @GetMapping("/{boardId}") + public ResponseEntity> getBoardDetail(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long boardId) { + return ResponseEntity.ok(SuccessResponse.create(GET_BOARD_DETAIL_SUCCESS.getMessage(), this.getBoardUseCase.getBoardDetail(user.getSocialId(), teamId, boardId))); + } + + /** + * 게시글 전체 조회 + * [GET] api/{teamId}/board + * 작성자 : 김민수 + */ + @GetMapping + public ResponseEntity> getBoardAll(@AuthenticationPrincipal User user, + @PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(GET_BOARD_ALL_SUCCESS.getMessage(), this.getBoardUseCase.getAllBoard(user.getSocialId(), teamId))); + } + +} diff --git a/src/main/java/com/moing/backend/domain/board/presentation/constant/BoardResponseMessage.java b/src/main/java/com/moing/backend/domain/board/presentation/constant/BoardResponseMessage.java new file mode 100644 index 00000000..da90a821 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/board/presentation/constant/BoardResponseMessage.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.board.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BoardResponseMessage { + CREATE_BOARD_SUCCESS("게시글을 생성했습니다."), + GET_BOARD_ALL_SUCCESS("게시글 목록을 모두 조회했습니다."), + UPDATE_BOARD_SUCCESS("게시글을 수정했습니다."), + GET_BOARD_DETAIL_SUCCESS("게시글 상세 조회했습니다."), + DELETE_BOARD_SUCCESS("게시글을 삭제했습니다"); + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/mapper/BoardCommentMapper.java b/src/main/java/com/moing/backend/domain/boardComment/application/mapper/BoardCommentMapper.java new file mode 100644 index 00000000..00f9f751 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/application/mapper/BoardCommentMapper.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.boardComment.application.mapper; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class BoardCommentMapper { + public static BoardComment toBoardComment(TeamMember teamMember, Board board, CreateCommentRequest createCommentRequest, boolean isLeader) { + BoardComment boardComment= new BoardComment(); + boardComment.init(createCommentRequest.getContent(),isLeader); + boardComment.updateBoard(board); + boardComment.updateTeamMember(teamMember); + return boardComment; + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUseCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUseCase.java new file mode 100644 index 00000000..47fb22f0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/CreateBoardCommentUseCase.java @@ -0,0 +1,39 @@ +package com.moing.backend.domain.boardComment.application.service; + +import com.moing.backend.domain.boardComment.application.mapper.BoardCommentMapper; +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardComment.domain.service.BoardCommentSaveService; +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.comment.application.dto.response.CreateCommentResponse; +import com.moing.backend.domain.team.application.service.CheckLeaderUseCase; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import com.moing.backend.global.utils.BaseBoardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CreateBoardCommentUseCase { + + private final BoardCommentSaveService boardCommentSaveService; + private final BaseBoardService baseBoardService; + private final CheckLeaderUseCase checkLeaderUseCase; + private final SendBoardCommentAlarmUseCase sendCommentAlarm; + /** + * 게시글 댓글 생성 + */ + public CreateCommentResponse createBoardComment(String socialId, Long teamId, Long boardId, CreateCommentRequest createCommentRequest) { + // 1. 게시글 댓글 생성 + BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); + boolean isLeader = checkLeaderUseCase.isTeamLeader(data.getMember(), data.getTeam()); + BoardComment boardComment = boardCommentSaveService.saveComment(BoardCommentMapper.toBoardComment(data.getTeamMember(), data.getBoard(), createCommentRequest, isLeader)); + // 2. 게시글 댓글 개수 증가 + data.getBoard().incrComNum(); + // 3. 게시글 댓글 알림 + sendCommentAlarm.sendCommentAlarm(data, boardComment); + return new CreateCommentResponse(boardComment.getBoardCommentId()); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUseCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUseCase.java new file mode 100644 index 00000000..7c5a4611 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/DeleteBoardCommentUseCase.java @@ -0,0 +1,39 @@ +package com.moing.backend.domain.boardComment.application.service; + +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardComment.domain.service.BoardCommentDeleteService; +import com.moing.backend.domain.boardComment.domain.service.BoardCommentGetService; +import com.moing.backend.domain.boardComment.exception.NotAuthByBoardCommentException; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import com.moing.backend.global.utils.BaseBoardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class DeleteBoardCommentUseCase { + + private final BoardCommentGetService boardCommentGetService; + private final BoardCommentDeleteService boardCommentDeleteService; + private final BaseBoardService baseBoardService; + + /** + * 게시글 댓글 삭제 + */ + + public void deleteBoardComment(String socialId, Long teamId, Long boardId, Long boardCommentId){ + // 1. 게시글 댓글 조회 + BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); + BoardComment boardComment=boardCommentGetService.getComment(boardCommentId); + // 2. 게시글 댓글 작성자만 + if (data.getTeamMember() == boardComment.getTeamMember()) { + // 3. 삭제 + boardCommentDeleteService.deleteComment(boardComment); + // 4. 댓글 개수 줄이기 + data.getBoard().decrComNum(); + } else throw new NotAuthByBoardCommentException(); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUseCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUseCase.java new file mode 100644 index 00000000..d34816cd --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/GetBoardCommentUseCase.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.boardComment.application.service; + +import com.moing.backend.domain.boardComment.domain.service.BoardCommentGetService; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import com.moing.backend.global.utils.BaseBoardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class GetBoardCommentUseCase { + + private final BoardCommentGetService boardCommentGetService; + private final BaseBoardService baseBoardService; + + /** + * 게시글 댓글 전체 조회 + */ + public GetCommentResponse getBoardCommentAll(String socialId, Long teamId, Long boardId){ + BaseBoardServiceResponse data = baseBoardService.getCommonData(socialId, teamId, boardId); + return boardCommentGetService.getCommentAll(boardId, data.getTeamMember()); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/application/service/SendBoardCommentAlarmUseCase.java b/src/main/java/com/moing/backend/domain/boardComment/application/service/SendBoardCommentAlarmUseCase.java new file mode 100644 index 00000000..7b6ae5e3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/application/service/SendBoardCommentAlarmUseCase.java @@ -0,0 +1,86 @@ +package com.moing.backend.domain.boardComment.application.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardComment.domain.service.BoardCommentGetService; +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent; +import com.moing.backend.global.config.fcm.dto.event.SingleFcmEvent; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.moing.backend.global.config.fcm.constant.NewCommentUploadMessage.NEW_COMMENT_UPLOAD_MESSAGE; + +@Service +@RequiredArgsConstructor +@Transactional +public class SendBoardCommentAlarmUseCase { + + private final ApplicationEventPublisher eventPublisher; + private final BoardCommentGetService boardCommentGetService; + + public void sendCommentAlarm(BaseBoardServiceResponse response, BoardComment comment) { + Member member = response.getMember(); + Team team = response.getTeam(); + Board board = response.getBoard(); + + Optional> newUploadInfos = boardCommentGetService.getNewUploadInfo(member.getMemberId(), board.getBoardId()); + String title = NEW_COMMENT_UPLOAD_MESSAGE.title(comment.getContent()); + String body = NEW_COMMENT_UPLOAD_MESSAGE.body(member.getNickName(),board.getTitle()); + + sendBoardCommentWriter(board, member, title, body, team, newUploadInfos); + sendBoardWriter(board, member, title, body, team, newUploadInfos); + } + + private void sendBoardWriter(Board board, Member member, String title, String body, Team team, Optional> newUploadInfos) { + Member receiver = board.getTeamMember().getMember(); + + if (checkBoardWriter(receiver, member, newUploadInfos)) { + eventPublisher.publishEvent(new SingleFcmEvent(receiver, title, body, createIdInfo(team.getTeamId(), board.getBoardId()), team.getName(), AlarmType.COMMENT, PagePath.NOTICE_PATH.getValue(), receiver.isCommentPush())); + } + } + + private void sendBoardCommentWriter(Board board, Member member, String title, String body, Team team, Optional> newUploadInfos) { + Optional> memberIdAndTokensByPush = AlarmHistoryMapper.getNewUploadPushInfo(newUploadInfos); + Optional> memberIdAndTokensBySave = AlarmHistoryMapper.getNewUploadSaveInfo(newUploadInfos); + + eventPublisher.publishEvent(new MultiFcmEvent(title, body, memberIdAndTokensByPush, memberIdAndTokensBySave, createIdInfo(team.getTeamId(), board.getBoardId()), team.getName(), AlarmType.COMMENT, PagePath.NOTICE_PATH.getValue())); + } + + private String createIdInfo(Long teamId, Long boardId) { + JSONObject jo = new JSONObject(); + jo.put("teamId", teamId); + jo.put("boardId", boardId); + jo.put("type", "COMMENT_BOARD"); + return jo.toJSONString(); + } + + private boolean checkBoardWriter(Member boardWriter, Member commentWriter, Optional> newUploadInfos) { + // 댓글 작성자와 게시글 작성자가 동일한 경우 알림을 보내지 않는다. + if (Objects.equals(boardWriter.getMemberId(), commentWriter.getMemberId())) { + return false; + } + + // newUploadInfos 리스트에 boardWriter의 memberId가 없는 경우만 알림을 보낸다. + // 리스트가 비어있거나, boardWriter의 memberId가 리스트에 없으면 true를 반환. + return newUploadInfos + .map(infos -> infos.stream().noneMatch(info -> info.getMemberId().equals(boardWriter.getMemberId()))) + .orElse(true); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/entity/BoardComment.java b/src/main/java/com/moing/backend/domain/boardComment/domain/entity/BoardComment.java new file mode 100644 index 00000000..6b39e57e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/entity/BoardComment.java @@ -0,0 +1,53 @@ +package com.moing.backend.domain.boardComment.domain.entity; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.comment.domain.entity.Comment; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class BoardComment extends Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "board_comment_id") + private Long boardCommentId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_member_id") + private TeamMember teamMember; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id") + private Board board; + + /** + * 연관관계 매핑 + */ + public void updateBoard(Board board) { + this.board = board; + board.getBoardComments().add(this); + } + + public void updateTeamMember(TeamMember teamMember) { + this.teamMember = teamMember; + } + + public void init(String content, boolean isLeader){ + this.content=content; + this.isLeader=isLeader; + } + + public String getWriterNickName(){ + return teamMember.getMemberNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentCustomRepository.java b/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentCustomRepository.java new file mode 100644 index 00000000..8040bf28 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentCustomRepository.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.boardComment.domain.repository; + +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; + +import java.util.List; +import java.util.Optional; + +public interface BoardCommentCustomRepository { + GetCommentResponse findBoardCommentAll(Long boardId, TeamMember teamMember); + + Optional> findNewUploadInfo(Long memberId, Long boardId); +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentCustomRepositoryImpl.java new file mode 100644 index 00000000..9ac674c3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentCustomRepositoryImpl.java @@ -0,0 +1,86 @@ +package com.moing.backend.domain.boardComment.domain.repository; + +import com.moing.backend.domain.block.domain.repository.BlockRepositoryUtils; +import com.moing.backend.domain.comment.application.dto.response.CommentBlocks; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.comment.application.dto.response.QCommentBlocks; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.teamMember.domain.entity.QTeamMember; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.boardComment.domain.entity.QBoardComment.boardComment; +import static com.moing.backend.domain.member.domain.entity.QMember.member; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; + +public class BoardCommentCustomRepositoryImpl implements BoardCommentCustomRepository{ + + private final JPAQueryFactory queryFactory; + + public BoardCommentCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + + @Override + public GetCommentResponse findBoardCommentAll(Long boardId, TeamMember teamMember) { + + BooleanExpression blockCondition = BlockRepositoryUtils.blockCondition(teamMember.getTeamMemberId(), boardComment.teamMember.member.memberId); + + List commentBlocks = queryFactory + .select(new QCommentBlocks( + boardComment.boardCommentId, + boardComment.content, + boardComment.teamMember.member.nickName, + boardComment.isLeader, + boardComment.teamMember.member.profileImage, + ExpressionUtils.as(JPAExpressions + .selectOne() + .from(QTeamMember.teamMember) + .where(QTeamMember.teamMember.eq(teamMember) + .and(QTeamMember.teamMember.eq(boardComment.teamMember))) + .exists(), "isWriter"), + boardComment.teamMember.isDeleted, + boardComment.createdDate, + boardComment.teamMember.member.memberId)) + .from(boardComment) + .leftJoin(boardComment.teamMember, QTeamMember.teamMember) + .leftJoin(boardComment.teamMember.member, member) + .where(boardComment.board.boardId.eq(boardId) + .and(blockCondition)) + .orderBy(boardComment.createdDate.asc()) + .fetch(); + + return new GetCommentResponse(commentBlocks); + } + + @Override + public Optional> findNewUploadInfo(Long memberId, Long boardId) { + BooleanExpression blockCondition= BlockRepositoryUtils.blockCondition(boardComment.teamMember.member.memberId, memberId); + + List result = queryFactory.select(Projections.constructor(NewUploadInfo.class, + boardComment.teamMember.member.fcmToken, + boardComment.teamMember.member.memberId, + boardComment.teamMember.member.isCommentPush, + boardComment.teamMember.member.isSignOut)) + .distinct() + .from(boardComment) + .leftJoin(boardComment.teamMember, teamMember) + .leftJoin(boardComment.teamMember.member, member) + .where(boardComment.board.boardId.eq(boardId) //게시글의 댓글인데 + .and(boardComment.teamMember.member.memberId.ne(memberId)) //나는 포함 안하고 + .and(boardComment.teamMember.isDeleted.eq(false)) //탈퇴한 사람도 포함 안함 + .and(blockCondition)) + .fetch(); + + return result.isEmpty() ? Optional.empty() : Optional.of(result); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentRepository.java b/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentRepository.java new file mode 100644 index 00000000..99a7b2a3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/repository/BoardCommentRepository.java @@ -0,0 +1,10 @@ +package com.moing.backend.domain.boardComment.domain.repository; + +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BoardCommentRepository extends JpaRepository, BoardCommentCustomRepository { + Optional findBoardCommentByBoardCommentId(Long boardCommentId); +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentDeleteService.java b/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentDeleteService.java new file mode 100644 index 00000000..63e55f09 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentDeleteService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.boardComment.domain.service; + +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardComment.domain.repository.BoardCommentRepository; +import com.moing.backend.domain.comment.domain.service.CommentDeleteService; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class BoardCommentDeleteService implements CommentDeleteService { + private final BoardCommentRepository boardCommentRepository; + + @Override + public void deleteComment(BoardComment boardComment){ + this.boardCommentRepository.delete(boardComment); + } + +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentGetService.java b/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentGetService.java new file mode 100644 index 00000000..1f2eaea0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentGetService.java @@ -0,0 +1,38 @@ +package com.moing.backend.domain.boardComment.domain.service; + +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardComment.domain.repository.BoardCommentRepository; +import com.moing.backend.domain.boardComment.exception.NotFoundByBoardCommentIdException; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.comment.domain.service.CommentGetService; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class BoardCommentGetService implements CommentGetService { + + private final BoardCommentRepository boardCommentRepository; + + @Override + public BoardComment getComment(Long boardCommentId){ + return boardCommentRepository.findBoardCommentByBoardCommentId(boardCommentId).orElseThrow(NotFoundByBoardCommentIdException::new); + } + + @Override + public GetCommentResponse getCommentAll(Long boardId, TeamMember teamMember){ + return boardCommentRepository.findBoardCommentAll(boardId, teamMember); + } + + @Override + public Optional> getNewUploadInfo(Long memberId, Long boardId) { + return boardCommentRepository.findNewUploadInfo(memberId, boardId); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentSaveService.java b/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentSaveService.java new file mode 100644 index 00000000..f4ac6d04 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/domain/service/BoardCommentSaveService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.boardComment.domain.service; + +import com.moing.backend.domain.boardComment.domain.repository.BoardCommentRepository; +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.comment.domain.service.CommentSaveService; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class BoardCommentSaveService implements CommentSaveService { + + private final BoardCommentRepository boardCommentRepository; + + @Override + public BoardComment saveComment(BoardComment boardComment){ + return this.boardCommentRepository.save(boardComment); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/exception/BoardCommentException.java b/src/main/java/com/moing/backend/domain/boardComment/exception/BoardCommentException.java new file mode 100644 index 00000000..6a7778f6 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/exception/BoardCommentException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.boardComment.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class BoardCommentException extends ApplicationException { + protected BoardCommentException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/boardComment/exception/NotAuthByBoardCommentException.java b/src/main/java/com/moing/backend/domain/boardComment/exception/NotAuthByBoardCommentException.java new file mode 100644 index 00000000..4743776a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/exception/NotAuthByBoardCommentException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.boardComment.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotAuthByBoardCommentException extends BoardCommentException { + public NotAuthByBoardCommentException() { + super(ErrorCode.NOT_AUTH_BY_BOARD_COMMENT_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/exception/NotFoundByBoardCommentIdException.java b/src/main/java/com/moing/backend/domain/boardComment/exception/NotFoundByBoardCommentIdException.java new file mode 100644 index 00000000..256aa3e1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/exception/NotFoundByBoardCommentIdException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.boardComment.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundByBoardCommentIdException extends BoardCommentException { + public NotFoundByBoardCommentIdException() { + super(ErrorCode.NOT_FOUND_BY_BOARD_COMMENT_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/presentattion/BoardCommentController.java b/src/main/java/com/moing/backend/domain/boardComment/presentattion/BoardCommentController.java new file mode 100644 index 00000000..501f0cfd --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/presentattion/BoardCommentController.java @@ -0,0 +1,69 @@ +package com.moing.backend.domain.boardComment.presentattion; + +import com.moing.backend.domain.boardComment.application.service.CreateBoardCommentUseCase; +import com.moing.backend.domain.boardComment.application.service.DeleteBoardCommentUseCase; +import com.moing.backend.domain.boardComment.application.service.GetBoardCommentUseCase; +import com.moing.backend.domain.boardComment.presentattion.constant.BoardCommentResponseMessage; +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.comment.application.dto.response.CreateCommentResponse; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +import static com.moing.backend.domain.boardComment.presentattion.constant.BoardCommentResponseMessage.GET_BOARD_COMMENT_ALL_SUCCESS; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/{teamId}/{boardId}/comment") +public class BoardCommentController { + + private final CreateBoardCommentUseCase createBoardCommentUseCase; + private final DeleteBoardCommentUseCase deleteBoardCommentUseCase; + private final GetBoardCommentUseCase getBoardCommentUseCase; + + /** + * 댓글 생성 + * [POST] api/{teamId}/{boardId}/comment + * 작성자 : 김민수 + */ + @PostMapping + public ResponseEntity> createBoardComment(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long boardId, + @Valid @RequestBody CreateCommentRequest createCommentRequest) { + return ResponseEntity.ok(SuccessResponse.create(BoardCommentResponseMessage.CREATE_BOARD_COMMENT_SUCCESS.getMessage(), this.createBoardCommentUseCase.createBoardComment(user.getSocialId(), teamId, boardId, createCommentRequest))); + } + + /** + * 댓글 삭제 + * [DELETE] api/{teamId}/{boardId}/comment/{commentId} + * 작성자 : 김민수 + */ + @DeleteMapping("/{commentId}") + public ResponseEntity deleteBoardComment(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long boardId, + @PathVariable Long commentId) { + this.deleteBoardCommentUseCase.deleteBoardComment(user.getSocialId(), teamId, boardId, commentId); + return ResponseEntity.ok(SuccessResponse.create(BoardCommentResponseMessage.DELETE_BOARD_COMMENT_SUCCESS.getMessage())); + } + + + /** + * 댓글 전체 조회 + * [GET] api/{teamId}/{boardId}/comment + * 작성자 : 김민수 + */ + @GetMapping + public ResponseEntity> getBoardCommentAll(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long boardId) { + return ResponseEntity.ok(SuccessResponse.create(GET_BOARD_COMMENT_ALL_SUCCESS.getMessage(), this.getBoardCommentUseCase.getBoardCommentAll(user.getSocialId(), teamId, boardId))); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardComment/presentattion/constant/BoardCommentResponseMessage.java b/src/main/java/com/moing/backend/domain/boardComment/presentattion/constant/BoardCommentResponseMessage.java new file mode 100644 index 00000000..0cf29554 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardComment/presentattion/constant/BoardCommentResponseMessage.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.boardComment.presentattion.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BoardCommentResponseMessage { + CREATE_BOARD_COMMENT_SUCCESS("댓글을 생성했습니다."), + GET_BOARD_COMMENT_ALL_SUCCESS("댓글 목록을 모두 조회했습니다."), + DELETE_BOARD_COMMENT_SUCCESS("댓글을 삭제했습니다"); + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/boardRead/application/mapper/BoardReadMapper.java b/src/main/java/com/moing/backend/domain/boardRead/application/mapper/BoardReadMapper.java new file mode 100644 index 00000000..9f58858c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardRead/application/mapper/BoardReadMapper.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.boardRead.application.mapper; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.boardRead.domain.entity.BoardRead; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.stereotype.Component; + +@Component +public class BoardReadMapper { + public static BoardRead toBoardRead(Team team, Member member){ + BoardRead boardRead=new BoardRead(); + boardRead.updateTeam(team); + boardRead.updateMember(member); + return boardRead; + } +} diff --git a/src/main/java/com/moing/backend/domain/boardRead/application/service/CreateBoardReadUseCase.java b/src/main/java/com/moing/backend/domain/boardRead/application/service/CreateBoardReadUseCase.java new file mode 100644 index 00000000..efb39fc1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardRead/application/service/CreateBoardReadUseCase.java @@ -0,0 +1,25 @@ +package com.moing.backend.domain.boardRead.application.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.boardRead.application.mapper.BoardReadMapper; +import com.moing.backend.domain.boardRead.domain.entity.BoardRead; +import com.moing.backend.domain.boardRead.domain.service.BoardReadSaveService; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class CreateBoardReadUseCase { + + private final BoardReadSaveService boardReadSaveService; + + public void createBoardRead(Team team, Member member, Board board){ + BoardRead boardRead = BoardReadMapper.toBoardRead(team, member); + boardReadSaveService.saveBoardRead(board, boardRead); + } +} diff --git a/src/main/java/com/moing/backend/domain/boardRead/domain/entity/BoardRead.java b/src/main/java/com/moing/backend/domain/boardRead/domain/entity/BoardRead.java new file mode 100644 index 00000000..63ea5c98 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardRead/domain/entity/BoardRead.java @@ -0,0 +1,54 @@ +package com.moing.backend.domain.boardRead.domain.entity; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class BoardRead extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "board_read_id") + private Long boardReadId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id") + private Board board; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team__id") + private Team team; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "Member_id") + private Member member; + + + /** + * 연관관계 매핑 + */ + public void updateBoard(Board board) { + this.board = board; + board.getBoardReads().add(this); + } + + public void updateTeam(Team team) { + this.team = team; + } + + public void updateMember(Member member) { + this.member = member; + } +} diff --git a/src/main/java/com/moing/backend/domain/boardRead/domain/repository/BoardReadRepository.java b/src/main/java/com/moing/backend/domain/boardRead/domain/repository/BoardReadRepository.java new file mode 100644 index 00000000..81724604 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardRead/domain/repository/BoardReadRepository.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.boardRead.domain.repository; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.boardRead.domain.entity.BoardRead; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; + +import javax.persistence.LockModeType; +import java.util.List; +import java.util.Optional; + +public interface BoardReadRepository extends JpaRepository { + + List findBoardReadByBoardAndMemberAndTeam(Board board, Member member, Team team); +} diff --git a/src/main/java/com/moing/backend/domain/boardRead/domain/service/BoardReadSaveService.java b/src/main/java/com/moing/backend/domain/boardRead/domain/service/BoardReadSaveService.java new file mode 100644 index 00000000..2ea4112e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/boardRead/domain/service/BoardReadSaveService.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.boardRead.domain.service; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.boardRead.domain.entity.BoardRead; +import com.moing.backend.domain.boardRead.domain.repository.BoardReadRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.List; + +@DomainService +@RequiredArgsConstructor +@Transactional +public class BoardReadSaveService { + + private final BoardReadRepository boardReadRepository; + + + public void saveBoardRead(Board board, BoardRead boardRead) { + List existingBoardReads = boardReadRepository.findBoardReadByBoardAndMemberAndTeam(board, boardRead.getMember(), boardRead.getTeam()); + + if (existingBoardReads.isEmpty()) { + boardRead.updateBoard(board); + boardReadRepository.save(boardRead); + } + } +} diff --git a/src/main/java/com/moing/backend/domain/comment/application/dto/request/CreateCommentRequest.java b/src/main/java/com/moing/backend/domain/comment/application/dto/request/CreateCommentRequest.java new file mode 100644 index 00000000..84d79633 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/application/dto/request/CreateCommentRequest.java @@ -0,0 +1,21 @@ +package com.moing.backend.domain.comment.application.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class CreateCommentRequest { + + @NotBlank(message = "content 을 입력해 주세요.") + @Size(min = 0, max = 300, message = "댓글 글자수를 초과했습니다.") + private String content; +} + diff --git a/src/main/java/com/moing/backend/domain/comment/application/dto/response/CommentBlocks.java b/src/main/java/com/moing/backend/domain/comment/application/dto/response/CommentBlocks.java new file mode 100644 index 00000000..40164051 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/application/dto/response/CommentBlocks.java @@ -0,0 +1,59 @@ +package com.moing.backend.domain.comment.application.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Getter +@Builder +@AllArgsConstructor +public class CommentBlocks { + + private Long commentId; + + private String content; + + private String writerNickName; + + private Boolean writerIsLeader; + + private String writerProfileImage; + + private Boolean isWriter; + + private Boolean writerIsDeleted; + + private String createdDate; + + private Long makerId; + + @QueryProjection + public CommentBlocks(Long commentId, String content, String writerNickName, Boolean writerIsLeader, String writerProfileImage, Boolean isWriter, Boolean writerIsDeleted, LocalDateTime createdDate, Long makerId) { + this.commentId = commentId; + this.writerNickName = writerNickName; + this.writerIsLeader = writerIsLeader; + this.writerProfileImage = writerProfileImage; + this.content = content; + this.isWriter = isWriter; + this.writerIsDeleted=writerIsDeleted; + this.createdDate = getFormattedDate(createdDate); + this.makerId = makerId; + deleteMember(); + } + + public void deleteMember() { + if (Boolean.TRUE.equals(writerIsDeleted)) { + this.writerNickName = "(알 수 없음)"; + this.writerProfileImage = null; + } + } + + public String getFormattedDate(LocalDateTime localDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + return localDateTime.format(formatter); + } +} diff --git a/src/main/java/com/moing/backend/domain/comment/application/dto/response/CreateCommentResponse.java b/src/main/java/com/moing/backend/domain/comment/application/dto/response/CreateCommentResponse.java new file mode 100644 index 00000000..b392489c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/application/dto/response/CreateCommentResponse.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.comment.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class CreateCommentResponse { + private Long commentId; +} diff --git a/src/main/java/com/moing/backend/domain/comment/application/dto/response/GetCommentResponse.java b/src/main/java/com/moing/backend/domain/comment/application/dto/response/GetCommentResponse.java new file mode 100644 index 00000000..06df8937 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/application/dto/response/GetCommentResponse.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.comment.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class GetCommentResponse { + private List commentBlocks; +} diff --git a/src/main/java/com/moing/backend/domain/comment/domain/entity/Comment.java b/src/main/java/com/moing/backend/domain/comment/domain/entity/Comment.java new file mode 100644 index 00000000..5410bf33 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/domain/entity/Comment.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.comment.domain.entity; + +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.Getter; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class Comment extends BaseTimeEntity { + + @Column(nullable = false, length = 300) + protected String content; + + protected boolean isLeader; /*작성자 소모임장유무*/ + public void updateContent(String content) { + this.content = content; + } + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/comment/domain/service/CommentDeleteService.java b/src/main/java/com/moing/backend/domain/comment/domain/service/CommentDeleteService.java new file mode 100644 index 00000000..1bd99c90 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/domain/service/CommentDeleteService.java @@ -0,0 +1,5 @@ +package com.moing.backend.domain.comment.domain.service; + +public interface CommentDeleteService { + void deleteComment(T comment); +} diff --git a/src/main/java/com/moing/backend/domain/comment/domain/service/CommentGetService.java b/src/main/java/com/moing/backend/domain/comment/domain/service/CommentGetService.java new file mode 100644 index 00000000..3f6cb854 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/domain/service/CommentGetService.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.comment.domain.service; + +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; + +import java.util.List; +import java.util.Optional; + +public interface CommentGetService { + T getComment(Long commentId); + GetCommentResponse getCommentAll(Long boardId, TeamMember teamMember); + Optional> getNewUploadInfo(Long memberId, Long boardId); +} diff --git a/src/main/java/com/moing/backend/domain/comment/domain/service/CommentSaveService.java b/src/main/java/com/moing/backend/domain/comment/domain/service/CommentSaveService.java new file mode 100644 index 00000000..b0a73f8b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/comment/domain/service/CommentSaveService.java @@ -0,0 +1,5 @@ +package com.moing.backend.domain.comment.domain.service; + +public interface CommentSaveService { + T saveComment(T comment); +} diff --git a/src/main/java/com/moing/backend/domain/fire/application/dto/req/FireThrowReq.java b/src/main/java/com/moing/backend/domain/fire/application/dto/req/FireThrowReq.java new file mode 100644 index 00000000..34907e1b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/application/dto/req/FireThrowReq.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.fire.application.dto.req; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class FireThrowReq { + + private String message; + + @Builder + public FireThrowReq(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/application/dto/res/FireReceiveRes.java b/src/main/java/com/moing/backend/domain/fire/application/dto/res/FireReceiveRes.java new file mode 100644 index 00000000..337d90ac --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/application/dto/res/FireReceiveRes.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.fire.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@AllArgsConstructor +public class FireReceiveRes { + + private Long receiveMemberId; + private String nickname; + private String fireStatus; + private String profileImg; + + public FireReceiveRes(Long receiveMemberId, String nickname,String profileImg) { + this.receiveMemberId = receiveMemberId; + this.nickname = nickname; + this.profileImg = profileImg; + } + + public void updateFireStatus(boolean status) { + if (status) + this.fireStatus = "True"; + else this.fireStatus = "False"; + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/application/dto/res/FireThrowRes.java b/src/main/java/com/moing/backend/domain/fire/application/dto/res/FireThrowRes.java new file mode 100644 index 00000000..12d19db3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/application/dto/res/FireThrowRes.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.fire.application.dto.res; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FireThrowRes { + + private Long receiveMemberId; +} diff --git a/src/main/java/com/moing/backend/domain/fire/application/mapper/FireMapper.java b/src/main/java/com/moing/backend/domain/fire/application/mapper/FireMapper.java new file mode 100644 index 00000000..c8a803b7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/application/mapper/FireMapper.java @@ -0,0 +1,34 @@ +package com.moing.backend.domain.fire.application.mapper; + +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; +import com.moing.backend.domain.fire.application.dto.res.FireThrowRes; +import com.moing.backend.domain.fire.domain.entity.Fire; +import com.moing.backend.domain.member.domain.entity.Member; + +import java.util.ArrayList; +import java.util.List; + +public class FireMapper { + + public static FireThrowRes mapToFireThrowRes(Fire fire) { + return FireThrowRes.builder() + .receiveMemberId(fire.getReceiveMemberId()) + .build(); + } + + public static List mapToFireReceiversList(List members) { + List fireReceiveResList = new ArrayList<>(); + members.forEach( + member -> fireReceiveResList.add(FireMapper.mapToFireReceiveRes(member)) + ); + return fireReceiveResList; + } + + public static FireReceiveRes mapToFireReceiveRes(Member member) { + return FireReceiveRes.builder() + .receiveMemberId(member.getMemberId()) + .nickname(member.getNickName()) + .fireStatus("TRUE") + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/application/service/FireThrowAlarmUseCase.java b/src/main/java/com/moing/backend/domain/fire/application/service/FireThrowAlarmUseCase.java new file mode 100644 index 00000000..0566dea0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/application/service/FireThrowAlarmUseCase.java @@ -0,0 +1,79 @@ +package com.moing.backend.domain.fire.application.service; + +import com.moing.backend.domain.fire.application.dto.req.FireThrowReq; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.config.fcm.dto.event.SingleFcmEvent; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.Random; + +import static com.moing.backend.domain.history.domain.entity.AlarmType.FIRE; +import static com.moing.backend.domain.history.domain.entity.PagePath.MISSION_PATH; +import static com.moing.backend.global.config.fcm.constant.FireThrowMessage.*; + +@Service +@Transactional +@RequiredArgsConstructor +public class FireThrowAlarmUseCase { + + private final ApplicationEventPublisher eventPublisher; + + public void sendFireThrowAlarm(Member throwMember, Member receiveMember, Team team, Mission mission, FireThrowReq fireThrowReq) { + + int randomNum = new Random(System.currentTimeMillis()).nextInt(2); + + String title = fireThrowReq != null ? NEW_FIRE_THROW_TITLE_WITH_COMMENT.to(throwMember.getNickName()) + : getRandomTitle(throwMember.getNickName(), receiveMember.getNickName(), randomNum); + String message = fireThrowReq != null ? fireThrowReq.getMessage() + : getRandomMessage(throwMember.getNickName(), receiveMember.getNickName(), randomNum); + String idInfo = createIdInfo(mission.getType() == MissionType.REPEAT, mission.getTeam().getTeamId(), mission.getId(), fireThrowReq == null); + + eventPublisher.publishEvent(new SingleFcmEvent(receiveMember, title, message, idInfo, team.getName(), FIRE, MISSION_PATH.getValue(), receiveMember.isFirePush())); + } + + public String getRandomMessage(String pusher, String receiver, int num) { + + switch (num) { + case 0: + return NEW_FIRE_THROW_MESSAGE1.fromTo(pusher, receiver); + case 1: return NEW_FIRE_THROW_MESSAGE2.toFrom(receiver, pusher); + + } + return NEW_FIRE_THROW_MESSAGE1.fromTo(pusher, receiver); + } + + public String getRandomTitle(String pusher, String receiver, int num) { + + switch (num) { + case 0: + return NEW_FIRE_THROW_TITLE1.getMessage(); + case 1: + return NEW_FIRE_THROW_TITLE2.getMessage(); + } + return NEW_FIRE_THROW_TITLE1.getMessage(); + } + + private String createIdInfo(boolean isRepeated, Long teamId, Long missionId, boolean isMessageNull) { + JSONObject jo = new JSONObject(); + jo.put("isRepeated", isRepeated); + jo.put("teamId", teamId); + jo.put("missionId", missionId); + jo.put("type", getType(isMessageNull)); + return jo.toJSONString(); + } + + private String getType(boolean isMessageNull){ + if(isMessageNull) + return "FIRE_MESSAGE_NULL"; + return "FIRE_MESSAGE_EXIST"; + } + + +} diff --git a/src/main/java/com/moing/backend/domain/fire/application/service/FireThrowUseCase.java b/src/main/java/com/moing/backend/domain/fire/application/service/FireThrowUseCase.java new file mode 100644 index 00000000..494a1c0d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/application/service/FireThrowUseCase.java @@ -0,0 +1,87 @@ +package com.moing.backend.domain.fire.application.service; + +import com.moing.backend.domain.fire.application.dto.req.FireThrowReq; +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; +import com.moing.backend.domain.fire.application.dto.res.FireThrowRes; +import com.moing.backend.domain.fire.application.mapper.FireMapper; +import com.moing.backend.domain.fire.domain.entity.Fire; +import com.moing.backend.domain.fire.domain.service.FireQueryService; +import com.moing.backend.domain.fire.domain.service.FireSaveService; +import com.moing.backend.domain.fire.exception.NoAuthThrowFireException; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +public class FireThrowUseCase { + + public final FireSaveService fireSaveService; + public final FireQueryService fireQueryService; + private final MemberGetService memberGetService; + private final FireThrowAlarmUseCase fireThrowAlarmUseCase; + private final MissionArchiveQueryService missionArchiveQueryService; + private final MissionQueryService missionQueryService; + private final TeamGetService teamGetService; + + public FireThrowRes createFireThrow(String userId, Long receiveMemberId, Long missionId, Long teamId, FireThrowReq fireThrowReq) { + + Member throwMember = memberGetService.getMemberBySocialId(userId); + Member receiveMember = memberGetService.getMemberByMemberId(receiveMemberId); + Team team = teamGetService.getTeamByTeamId(teamId); + Mission mission = missionQueryService.findMissionById(missionId); + + // 나에게 던질 수 없음 + if (throwMember.equals(receiveMember)) { + throw new NoAuthThrowFireException(); + } + + // 1시간전 불 던진 기록이 있다면, 던질 수 없음 + if (!fireQueryService.hasFireCreatedWithinOneHour(throwMember.getMemberId(), receiveMemberId)) { + throw new NoAuthThrowFireException(); + } + + fireThrowAlarmUseCase.sendFireThrowAlarm(throwMember, receiveMember, team, mission, fireThrowReq); + + Fire save = fireSaveService.save(Fire.builder() + .throwMemberId(throwMember.getMemberId()) + .receiveMemberId(receiveMemberId) + .build()); + + return FireMapper.mapToFireThrowRes(fireSaveService.save(save)); + } + + public List getFireReceiveList(String userId,Long teamId, Long missionId) { + Member member = memberGetService.getMemberBySocialId(userId); + Long memberId = member.getMemberId(); + + List fireReceiveRes = fireQueryService.getNotYetMissionMember(teamId, missionId,memberId); + fireReceiveRes.forEach( + res -> res.updateFireStatus(fireQueryService.hasFireCreatedWithinOneHour(memberId,res.getReceiveMemberId()) + )); + +// if (!missionArchiveQueryService.isDone(memberId, missionId)) { +// fireReceiveRes.add(0,FireReceiveRes.builder() +// .receiveMemberId(memberId) +// .nickname(member.getNickName()) +// .fireStatus("False") +// .build()); +// } + + return fireReceiveRes; + } + + + +} diff --git a/src/main/java/com/moing/backend/domain/fire/domain/entity/Fire.java b/src/main/java/com/moing/backend/domain/fire/domain/entity/Fire.java new file mode 100644 index 00000000..8d6fe05c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/domain/entity/Fire.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.fire.domain.entity; + +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Fire extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "fire_id") + private Long id; + + private Long throwMemberId; + + private Long receiveMemberId; + +} diff --git a/src/main/java/com/moing/backend/domain/fire/domain/repository/FireCustomRepository.java b/src/main/java/com/moing/backend/domain/fire/domain/repository/FireCustomRepository.java new file mode 100644 index 00000000..1f6304ec --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/domain/repository/FireCustomRepository.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.fire.domain.repository; + + +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; + +import java.util.List; +import java.util.Optional; + +public interface FireCustomRepository { + + boolean hasFireCreatedWithinOneHour(Long throwMemberId, Long receiveMemberId); + Optional> getFireReceivers(Long teamId, Long missionId, Long memberId); + Long getTodayFires(); + Long getYesterdayFires(); +} diff --git a/src/main/java/com/moing/backend/domain/fire/domain/repository/FireCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/fire/domain/repository/FireCustomRepositoryImpl.java new file mode 100644 index 00000000..5c5d3e30 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/domain/repository/FireCustomRepositoryImpl.java @@ -0,0 +1,172 @@ +package com.moing.backend.domain.fire.domain.repository; + +import com.moing.backend.domain.block.domain.repository.BlockRepositoryUtils; +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.fire.domain.entity.QFire.fire; +import static com.moing.backend.domain.mission.domain.entity.QMission.mission; +import static com.moing.backend.domain.missionArchive.domain.entity.QMissionArchive.missionArchive; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; +import static com.querydsl.jpa.JPAExpressions.select; + +public class FireCustomRepositoryImpl implements FireCustomRepository { + private final JPAQueryFactory queryFactory; + + public FireCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + + public boolean hasFireCreatedWithinOneHour(Long throwMemberId, Long receiveMemberId) { + + + LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1); // 현재 시간에서 1시간을 뺀 시간 + long count = queryFactory + .select() + .from(fire) + .where( + fire.throwMemberId.eq(throwMemberId), + fire.receiveMemberId.eq(receiveMemberId), + fire.createdDate.after(oneHourAgo) // createdDate가 oneHourAgo 이후인 데이터 + ) + .fetchCount(); + + return count <= 0; // 1시간 이내에 생성된 데이터가 존재하면 true를 반환, 그렇지 않으면 false 반환 + } + + public Optional> getFireReceivers(Long teamId, Long missionId, Long memberId) { + + JPQLQuery todayCompletedMemberOfRepeat = todayRepeatMissionDone(missionId); + + JPQLQuery completedMembersOfAll = allMissionDone(missionId); + + BooleanExpression blockCondition= BlockRepositoryUtils.blockCondition(memberId, teamMember.member.memberId); + + return Optional.ofNullable(queryFactory + .select(Projections.constructor(FireReceiveRes.class, + teamMember.member.memberId, + teamMember.member.nickName, + teamMember.member.profileImage + )) + .from(teamMember) + .where( + teamMember.team.teamId.eq(teamId), + teamMember.member.memberId.ne(memberId), + teamMember.member.memberId.notIn(completedMembersOfAll) + .and(teamMember.member.memberId.notIn(todayCompletedMemberOfRepeat)), + teamMember.isDeleted.ne(Boolean.TRUE), + blockCondition + ) + .fetch()); + + } + + @Override + public Long getTodayFires() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long todayFires = queryFactory + .selectFrom(fire) + .where(fire.createdDate.between(startOfToday, endOfToday)) + .fetchCount(); + + return todayFires; + } + + @Override + public Long getYesterdayFires(){ + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long yesterdayFires = queryFactory + .selectFrom(fire) + .where(fire.createdDate.between(startOfYesterday, startOfToday)) + .fetchCount(); + + return yesterdayFires; + } + + + private BooleanExpression createRepeatTypeConditionByArchive() { + LocalDate now = LocalDate.now(); + DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY; + LocalDate startOfWeek = now.with(TemporalAdjusters.previousOrSame(firstDayOfWeek)); + LocalDate endOfWeek = startOfWeek.plusDays(6); + + return missionArchive.createdDate.goe(startOfWeek.atStartOfDay()) + .and(missionArchive.createdDate.loe(endOfWeek.atStartOfDay().plusDays(1).minusNanos(1))); + } + + private BooleanExpression hasAlreadyVerifiedToday() { + LocalDateTime today = LocalDateTime.now(); + LocalDateTime startOfToday = today.withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfToday = today.withHour(23).withMinute(59).withSecond(59).withNano(999999999); + + return missionArchive.createdDate.between(startOfToday, endOfToday); + } + + private JPQLQuery todayRepeatMissionDone(Long missionId) { + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + BooleanExpression hasAlreadyVerifiedToday = hasAlreadyVerifiedToday(); + + return + select(missionArchive.member.memberId) + .from(missionArchive, mission) + .where(missionArchive.mission.id.eq(missionId), + mission.id.eq(missionId), + (missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange).and(hasAlreadyVerifiedToday)).or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .groupBy(missionArchive.member.memberId, + missionArchive.mission.id, + missionArchive.count, + mission.number, + missionArchive.mission.type, + missionArchive.createdDate) + .having( + missionArchive.mission.id.eq(missionId), + (missionArchive.mission.type.eq(MissionType.REPEAT)).or(missionArchive.mission.type.eq(MissionType.ONCE)) + ); + } + + private JPQLQuery allMissionDone(Long missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return + select(missionArchive.member.memberId) + .from(missionArchive, mission) + .where(missionArchive.mission.id.eq(missionId), + mission.id.eq(missionId), + (missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange)).or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .groupBy(missionArchive.member.memberId, + missionArchive.mission.id, + missionArchive.count, + mission.number) + .having( + missionArchive.mission.id.eq(missionId), + missionArchive.count.max().goe(mission.number) + ); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/fire/domain/repository/FireRepository.java b/src/main/java/com/moing/backend/domain/fire/domain/repository/FireRepository.java new file mode 100644 index 00000000..6a9b01af --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/domain/repository/FireRepository.java @@ -0,0 +1,7 @@ +package com.moing.backend.domain.fire.domain.repository; + +import com.moing.backend.domain.fire.domain.entity.Fire; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FireRepository extends JpaRepository,FireCustomRepository { +} diff --git a/src/main/java/com/moing/backend/domain/fire/domain/service/FireQueryService.java b/src/main/java/com/moing/backend/domain/fire/domain/service/FireQueryService.java new file mode 100644 index 00000000..1635607d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/domain/service/FireQueryService.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.fire.domain.service; + +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; +import com.moing.backend.domain.fire.domain.repository.FireRepository; +import com.moing.backend.domain.fire.exception.NotFoundFireReceiversException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FireQueryService { + + private final FireRepository fireRepository; + + public boolean hasFireCreatedWithinOneHour(Long throwMemberId, Long receiveMemberId) { + return fireRepository.hasFireCreatedWithinOneHour(throwMemberId,receiveMemberId); + } + + public List getNotYetMissionMember(Long teamId, Long missionId, Long memberId) { + return fireRepository.getFireReceivers(teamId, missionId,memberId).orElseThrow(NotFoundFireReceiversException::new); + } + + public Long getTodayFires(){ + return fireRepository.getTodayFires(); + } + public Long getYesterdayFires(){ + return fireRepository.getYesterdayFires(); + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/domain/service/FireSaveService.java b/src/main/java/com/moing/backend/domain/fire/domain/service/FireSaveService.java new file mode 100644 index 00000000..14a94b30 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/domain/service/FireSaveService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.fire.domain.service; + +import com.moing.backend.domain.fire.domain.entity.Fire; +import com.moing.backend.domain.fire.domain.repository.FireCustomRepository; +import com.moing.backend.domain.fire.domain.repository.FireRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class FireSaveService { + + private final FireRepository fireRepository; + + public Fire save(Fire fire) { + return fireRepository.save(fire); + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/exception/FireException.java b/src/main/java/com/moing/backend/domain/fire/exception/FireException.java new file mode 100644 index 00000000..9540cd09 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/exception/FireException.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.fire.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class FireException extends ApplicationException { + + protected FireException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/fire/exception/NoAuthThrowFireException.java b/src/main/java/com/moing/backend/domain/fire/exception/NoAuthThrowFireException.java new file mode 100644 index 00000000..fda8174d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/exception/NoAuthThrowFireException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.fire.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoAuthThrowFireException extends FireException { + + public NoAuthThrowFireException() { + super(ErrorCode.NOT_AUTH_FIRE_THROW, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/exception/NotFoundFireException.java b/src/main/java/com/moing/backend/domain/fire/exception/NotFoundFireException.java new file mode 100644 index 00000000..666d85b0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/exception/NotFoundFireException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.fire.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundFireException extends FireException { + + public NotFoundFireException() { + super(ErrorCode.NOT_FOUND_FIRE, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/exception/NotFoundFireReceiversException.java b/src/main/java/com/moing/backend/domain/fire/exception/NotFoundFireReceiversException.java new file mode 100644 index 00000000..b41c2eb7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/exception/NotFoundFireReceiversException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.fire.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundFireReceiversException extends FireException { + + public NotFoundFireReceiversException() { + super(ErrorCode.NOT_FOUND_FIRE_RECEIVERS, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/fire/presentation/FireController.java b/src/main/java/com/moing/backend/domain/fire/presentation/FireController.java new file mode 100644 index 00000000..76eb46a5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/presentation/FireController.java @@ -0,0 +1,54 @@ +package com.moing.backend.domain.fire.presentation; + +import com.moing.backend.domain.fire.application.dto.req.FireThrowReq; +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; +import com.moing.backend.domain.fire.application.dto.res.FireThrowRes; +import com.moing.backend.domain.fire.application.service.FireThrowUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.moing.backend.domain.fire.presentation.constant.FireResponseMessage.GET_RECEIVERS_SUCCESS; +import static com.moing.backend.domain.fire.presentation.constant.FireResponseMessage.THROW_FIRE_SUCCESS; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team/{teamId}/missions/{missionId}/fire") +public class FireController { + + private final FireThrowUseCase fireThrowUseCase; + + /** + * 불 던지기 + * [POST] {teamId}/mission/{missionId}/fire/{receiveMemberId} + * 작성자 : 정승연 + */ + + @PostMapping("/{receiveMemberId}") + public ResponseEntity> throwFire (@AuthenticationPrincipal User user, @PathVariable("teamId") Long teamId, + @PathVariable("receiveMemberId") Long receiveMemberId, @PathVariable("missionId") Long missionId, @RequestBody(required = false) FireThrowReq fireThrowReq) { + return ResponseEntity.ok(SuccessResponse.create(THROW_FIRE_SUCCESS.getMessage(), this.fireThrowUseCase.createFireThrow(user.getSocialId(), receiveMemberId, missionId, teamId, fireThrowReq))); + } + + /** + * 불 던질 사람 조회 (receivers) + * [GET] {teamId}/mission/{missionId}/fire + * 작성자 : 정승연 + */ + + @GetMapping() + public ResponseEntity>> throwFireList (@AuthenticationPrincipal User user, @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(GET_RECEIVERS_SUCCESS.getMessage(), this.fireThrowUseCase.getFireReceiveList(user.getSocialId(),teamId,missionId))); + } + + + + + +} diff --git a/src/main/java/com/moing/backend/domain/fire/presentation/constant/FireResponseMessage.java b/src/main/java/com/moing/backend/domain/fire/presentation/constant/FireResponseMessage.java new file mode 100644 index 00000000..8bbb91aa --- /dev/null +++ b/src/main/java/com/moing/backend/domain/fire/presentation/constant/FireResponseMessage.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.fire.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FireResponseMessage { + THROW_FIRE_SUCCESS("불던지기를 완료 했습니다"), + GET_RECEIVERS_SUCCESS("불 던질사람 조회를 완료 했습니다"); + + private final String message; + +} diff --git a/src/main/java/com/moing/backend/domain/history/application/dto/response/GetAlarmCountResponse.java b/src/main/java/com/moing/backend/domain/history/application/dto/response/GetAlarmCountResponse.java new file mode 100644 index 00000000..a0976ba7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/dto/response/GetAlarmCountResponse.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.history.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class GetAlarmCountResponse { + + private String count; +} diff --git a/src/main/java/com/moing/backend/domain/history/application/dto/response/GetAlarmHistoryResponse.java b/src/main/java/com/moing/backend/domain/history/application/dto/response/GetAlarmHistoryResponse.java new file mode 100644 index 00000000..d69e0d71 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/dto/response/GetAlarmHistoryResponse.java @@ -0,0 +1,62 @@ +package com.moing.backend.domain.history.application.dto.response; + +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Getter +@AllArgsConstructor +@Builder +public class GetAlarmHistoryResponse { + + private Long alarmHistoryId; + + private AlarmType type; + + private String path; + + private String idInfo; + + private String title; + + private String body; + + private String name; + + private Boolean isRead; + + private String createdDate; + + @QueryProjection + public GetAlarmHistoryResponse(Long alarmHistoryId, AlarmType type, String path, String idInfo, String title, String body, String name, boolean isRead, LocalDateTime createdDate) { + this.alarmHistoryId = alarmHistoryId; + this.type = type; + this.path = path; + this.idInfo = idInfo; + this.title = title; + this.body = body; + this.name = name; + this.isRead = isRead; + this.createdDate = formatCreatedDate(createdDate); + } + + public String formatCreatedDate(LocalDateTime createdDate) { + LocalDateTime currentDateTime = LocalDateTime.now(); + LocalDateTime midnightOfCreatedDate = createdDate.toLocalDate().atStartOfDay(); + + if (currentDateTime.isAfter(midnightOfCreatedDate.plusDays(1))) { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("M월 d일"); + return createdDate.format(dateFormatter); + } else { + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("a h:mm"); + return createdDate.format(timeFormatter); + } + } + + +} diff --git a/src/main/java/com/moing/backend/domain/history/application/dto/response/MemberIdAndToken.java b/src/main/java/com/moing/backend/domain/history/application/dto/response/MemberIdAndToken.java new file mode 100644 index 00000000..e8d303b0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/dto/response/MemberIdAndToken.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.history.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberIdAndToken { + + private String fcmToken; + private Long memberId; + +} diff --git a/src/main/java/com/moing/backend/domain/history/application/dto/response/NewUploadInfo.java b/src/main/java/com/moing/backend/domain/history/application/dto/response/NewUploadInfo.java new file mode 100644 index 00000000..5b5e2967 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/dto/response/NewUploadInfo.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.history.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NewUploadInfo { + + private String fcmToken; + private Long memberId; + private boolean isNewUploadPush; + private boolean isSignOut; + +} diff --git a/src/main/java/com/moing/backend/domain/history/application/mapper/AlarmHistoryMapper.java b/src/main/java/com/moing/backend/domain/history/application/mapper/AlarmHistoryMapper.java new file mode 100644 index 00000000..e6c2c83d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/mapper/AlarmHistoryMapper.java @@ -0,0 +1,77 @@ +package com.moing.backend.domain.history.application.mapper; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class AlarmHistoryMapper { + + public static AlarmHistory toAlarmHistory(AlarmType type, String path, String idInfo, Long receiverId, String title, String body, String name){ + return AlarmHistory.builder() + .type(type) + .path(path) + .idInfo(idInfo) + .receiverId(receiverId) + .title(title) + .body(body) + .name(name) + .isRead(false) + .build(); + } + + public static List getFcmTokens(List memberIdAndTokens) { + if (memberIdAndTokens == null || memberIdAndTokens.isEmpty()) { + return Collections.emptyList(); + } + + return memberIdAndTokens.stream() + .map(MemberIdAndToken::getFcmToken) + .collect(Collectors.toList()); + } + + + public static List getMemberIds(List memberIdAndTokens) { + if (memberIdAndTokens == null || memberIdAndTokens.isEmpty()) { + return Collections.emptyList(); + } + + return memberIdAndTokens.stream() + .map(MemberIdAndToken::getMemberId) + .collect(Collectors.toList()); + } + + public static List getAlarmHistories(String idInfo, List memberIds, String title, String body, String teamName, AlarmType alarmType, String path) { + return memberIds.stream() + .map(memberId -> toAlarmHistory(alarmType, path, idInfo, memberId, title, body, teamName)) + .collect(Collectors.toList()); + } + + public static Optional> getNewUploadSaveInfo(Optional> optionalNewUploadInfos) { + return optionalNewUploadInfos.map(newUploadInfos -> + newUploadInfos.stream() + .map(info -> new MemberIdAndToken(info.getFcmToken(), info.getMemberId())) + .collect(Collectors.toList()) + ); + } + + public static Optional> getNewUploadPushInfo(Optional> optionalNewUploadInfos) { + return optionalNewUploadInfos.map(newUploadInfos -> + newUploadInfos.stream() + .filter(NewUploadInfo::isNewUploadPush) + .filter(info -> !info.isSignOut()) + .map(info -> new MemberIdAndToken(info.getFcmToken(), info.getMemberId())) + .collect(Collectors.toList()) + ); + } + +} diff --git a/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java b/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java new file mode 100644 index 00000000..5692f1c4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/service/CleanupUseCase.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.history.application.service; + +import com.moing.backend.domain.history.domain.service.AlarmHistoryDeleteService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.concurrent.CompletableFuture; + +@Service +@RequiredArgsConstructor +@Profile("prod") +public class CleanupUseCase { + + private final AlarmHistoryDeleteService alarmHistoryDeleteService; + + @Scheduled(cron = "0 0 0 * * ?") + public void cleanupOldAlarmHistories() { + CompletableFuture.runAsync(() -> { + LocalDateTime oneWeekAgo = LocalDateTime.now().minusWeeks(1); + alarmHistoryDeleteService.deleteByCreatedDateBefore(oneWeekAgo); + }); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/application/service/GetAlarmHistoryUseCase.java b/src/main/java/com/moing/backend/domain/history/application/service/GetAlarmHistoryUseCase.java new file mode 100644 index 00000000..612c0eae --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/service/GetAlarmHistoryUseCase.java @@ -0,0 +1,38 @@ +package com.moing.backend.domain.history.application.service; + +import com.moing.backend.domain.history.application.dto.response.GetAlarmCountResponse; +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; +import com.moing.backend.domain.history.domain.service.AlarmHistoryGetService; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GetAlarmHistoryUseCase { + + private final MemberGetService memberGetService; + private final AlarmHistoryGetService alarmHistoryGetService; + + /** + * 알림 히스토리 조회 + */ + @Transactional + public List getAllAlarmHistories(String socialId) { + Member member = memberGetService.getMemberBySocialId(socialId); + return alarmHistoryGetService.getAlarmHistories(member.getMemberId()); + } + + /** + * 안 읽은 알림 개수 조회 + */ + @Transactional(readOnly = true) + public GetAlarmCountResponse getUnreadAlarmCount(String socialId) { + Member member = memberGetService.getMemberBySocialId(socialId); + return new GetAlarmCountResponse(alarmHistoryGetService.getUnreadAlarmCount(member.getMemberId())); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/application/service/ReadAlarmHistoryUseCase.java b/src/main/java/com/moing/backend/domain/history/application/service/ReadAlarmHistoryUseCase.java new file mode 100644 index 00000000..58c56347 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/service/ReadAlarmHistoryUseCase.java @@ -0,0 +1,30 @@ +package com.moing.backend.domain.history.application.service; + +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.service.AlarmHistoryGetService; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReadAlarmHistoryUseCase { + + private final MemberGetService memberGetService; + private final AlarmHistoryGetService alarmHistoryGetService; + + /** + * 알림 히스토리 읽기 + */ + @Transactional + public void readAlarmHistory(String socialId, Long alarmHistoryId) { + Member member = memberGetService.getMemberBySocialId(socialId); + AlarmHistory alarmHistory=alarmHistoryGetService.getAlarmHistory(alarmHistoryId, member.getMemberId()); + alarmHistory.readAlarmHistory(); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/application/service/SaveMultiAlarmHistoryUseCase.java b/src/main/java/com/moing/backend/domain/history/application/service/SaveMultiAlarmHistoryUseCase.java new file mode 100644 index 00000000..ab386647 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/service/SaveMultiAlarmHistoryUseCase.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.history.application.service; + +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.service.AlarmHistorySaveService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SaveMultiAlarmHistoryUseCase { + + private final AlarmHistorySaveService alarmHistorySaveService; + + @Async + public void saveAlarmHistories(List memberIds, String idInfo, String title, String body, String name, AlarmType alarmType, String path) { + List alarmHistories = AlarmHistoryMapper.getAlarmHistories(idInfo, memberIds, title, body, name, alarmType, path); + alarmHistorySaveService.saveAlarmHistories(alarmHistories); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/application/service/SaveSingleAlarmHistoryUseCase.java b/src/main/java/com/moing/backend/domain/history/application/service/SaveSingleAlarmHistoryUseCase.java new file mode 100644 index 00000000..90f1a63d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/application/service/SaveSingleAlarmHistoryUseCase.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.history.application.service; + +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.service.AlarmHistorySaveService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SaveSingleAlarmHistoryUseCase { + + private final AlarmHistorySaveService alarmHistorySaveService; + + @Async + public void saveAlarmHistory(Long memberId, String idInfo, String title, String body, String name, AlarmType alarmType, String path) { + AlarmHistory alarmHistory = AlarmHistoryMapper.toAlarmHistory(alarmType, path, idInfo, memberId, title, body, name); + alarmHistorySaveService.saveAlarmHistory(alarmHistory); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/entity/AlarmHistory.java b/src/main/java/com/moing/backend/domain/history/domain/entity/AlarmHistory.java new file mode 100644 index 00000000..723b23b6 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/entity/AlarmHistory.java @@ -0,0 +1,49 @@ +package com.moing.backend.domain.history.domain.entity; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AlarmHistory extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "alarm_history_id") + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private AlarmType type; + + @Column(nullable = false) + private String path; + + private String idInfo; + + private Long receiverId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String body; + + @Column(nullable = false) + private String name; + + private boolean isRead; + + public void readAlarmHistory(){ + this.isRead=true; + } +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/entity/AlarmType.java b/src/main/java/com/moing/backend/domain/history/domain/entity/AlarmType.java new file mode 100644 index 00000000..e29851bf --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/entity/AlarmType.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.history.domain.entity; + +import lombok.Getter; + +@Getter +public enum AlarmType { + NEW_UPLOAD, + FIRE, + REMIND, + APPROVE_TEAM, + REJECT_TEAM, + COMMENT +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/entity/PagePath.java b/src/main/java/com/moing/backend/domain/history/domain/entity/PagePath.java new file mode 100644 index 00000000..e597bc2a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/entity/PagePath.java @@ -0,0 +1,21 @@ +package com.moing.backend.domain.history.domain.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PagePath { + + NOTICE_PATH("/post/detail"), + + MISSION_PATH("/missions/prove"), + + MISSION_ALL_PTAH("/missions"), + + HOME_PATH("/home"); + + + private final String value; + +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryCustomRepository.java b/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryCustomRepository.java new file mode 100644 index 00000000..526a0e4b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryCustomRepository.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.history.domain.repository; + +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; + +import java.util.List; + +public interface AlarmHistoryCustomRepository { + + List findAlarmHistoriesByMemberId(Long memberId); + String findUnreadAlarmCount(Long memberId); +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryCustomRepositoryImpl.java new file mode 100644 index 00000000..2176d113 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryCustomRepositoryImpl.java @@ -0,0 +1,65 @@ +package com.moing.backend.domain.history.domain.repository; + +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; +import com.moing.backend.domain.history.application.dto.response.QGetAlarmHistoryResponse; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.querydsl.jpa.impl.JPAUpdateClause; + +import javax.persistence.EntityManager; +import java.util.List; + +import static com.moing.backend.domain.history.domain.entity.QAlarmHistory.alarmHistory; +import static com.moing.backend.domain.team.domain.entity.QTeam.team; + +public class AlarmHistoryCustomRepositoryImpl implements AlarmHistoryCustomRepository { + + private final JPAQueryFactory queryFactory; + + private final EntityManager em; + + public AlarmHistoryCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + this.em = em; + } + + @Override + public List findAlarmHistoriesByMemberId(Long memberId) { + List response= queryFactory.select(new QGetAlarmHistoryResponse(alarmHistory.id, + alarmHistory.type, + alarmHistory.path, + alarmHistory.idInfo, + alarmHistory.title, + alarmHistory.body, + alarmHistory.name, + alarmHistory.isRead, + alarmHistory.createdDate)) + .from(alarmHistory) + .where(alarmHistory.receiverId.eq(memberId)) + .orderBy(alarmHistory.createdDate.desc()) + .fetch(); + + readAlarmHistory(memberId); + return response; + } + + private void readAlarmHistory(Long memberId){ + queryFactory + .update(alarmHistory) + .set(alarmHistory.isRead, true) + .where(alarmHistory.receiverId.eq(memberId)) + .execute(); + + em.flush(); + em.clear(); + } + @Override + public String findUnreadAlarmCount(Long memberId) { + Long count = queryFactory.select(alarmHistory.count()) + .from(alarmHistory) + .where(alarmHistory.receiverId.eq(memberId) + .and(alarmHistory.isRead.eq(false))) + .fetchOne(); + + return count != null ? (count > 99 ? "99+" : count.toString()) : "0"; + } +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryRepository.java b/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryRepository.java new file mode 100644 index 00000000..5f2808c4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/repository/AlarmHistoryRepository.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.history.domain.repository; + +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDateTime; +import java.util.Optional; + +public interface AlarmHistoryRepository extends JpaRepository, AlarmHistoryCustomRepository { + + Optional findAlarmHistoryByIdAndReceiverId(Long id, Long receiverId); + void deleteByCreatedDateBefore(LocalDateTime dateTime); + +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistoryDeleteService.java b/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistoryDeleteService.java new file mode 100644 index 00000000..2a822728 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistoryDeleteService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.history.domain.service; + +import com.moing.backend.domain.history.domain.repository.AlarmHistoryRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class AlarmHistoryDeleteService { + + private final AlarmHistoryRepository alarmHistoryRepository; + + public void deleteByCreatedDateBefore(LocalDateTime oneWeekAgo) { + alarmHistoryRepository.deleteByCreatedDateBefore(oneWeekAgo); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistoryGetService.java b/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistoryGetService.java new file mode 100644 index 00000000..5f5ebae9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistoryGetService.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.history.domain.service; + +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.repository.AlarmHistoryRepository; +import com.moing.backend.domain.history.exception.NotFoundAlarmHistoryException; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.List; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class AlarmHistoryGetService { + + private final AlarmHistoryRepository alarmHistoryRepository; + + public List getAlarmHistories(Long memberId){ + return alarmHistoryRepository.findAlarmHistoriesByMemberId(memberId); + } + + public AlarmHistory getAlarmHistory(Long alarmHistoryId, Long memberId){ + return alarmHistoryRepository.findAlarmHistoryByIdAndReceiverId(alarmHistoryId, memberId).orElseThrow(NotFoundAlarmHistoryException::new); + } + + public String getUnreadAlarmCount(Long memberId){ + return alarmHistoryRepository.findUnreadAlarmCount(memberId); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistorySaveService.java b/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistorySaveService.java new file mode 100644 index 00000000..e758efff --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/domain/service/AlarmHistorySaveService.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.history.domain.service; + +import com.moing.backend.domain.history.domain.entity.AlarmHistory; +import com.moing.backend.domain.history.domain.repository.AlarmHistoryRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.List; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class AlarmHistorySaveService { + + private final AlarmHistoryRepository alarmHistoryRepository; + + public void saveAlarmHistories(List alarmHistories){ + alarmHistoryRepository.saveAll(alarmHistories); + } + + public void saveAlarmHistory(AlarmHistory alarmHistory){ + alarmHistoryRepository.save(alarmHistory); + } + +} diff --git a/src/main/java/com/moing/backend/domain/history/exception/AlarmHistoryException.java b/src/main/java/com/moing/backend/domain/history/exception/AlarmHistoryException.java new file mode 100644 index 00000000..2d9ee9a8 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/exception/AlarmHistoryException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.history.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class AlarmHistoryException extends ApplicationException { + protected AlarmHistoryException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/exception/NotFoundAlarmHistoryException.java b/src/main/java/com/moing/backend/domain/history/exception/NotFoundAlarmHistoryException.java new file mode 100644 index 00000000..ab77af9f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/exception/NotFoundAlarmHistoryException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.history.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundAlarmHistoryException extends AlarmHistoryException { + public NotFoundAlarmHistoryException() { + super(ErrorCode.NOT_FOUND_BY_ALARM_HISOTRY_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/history/presentation/AlarmHistoryController.java b/src/main/java/com/moing/backend/domain/history/presentation/AlarmHistoryController.java new file mode 100644 index 00000000..e5d183f3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/presentation/AlarmHistoryController.java @@ -0,0 +1,58 @@ +package com.moing.backend.domain.history.presentation; + +import com.moing.backend.domain.history.application.dto.response.GetAlarmCountResponse; +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; +import com.moing.backend.domain.history.application.service.GetAlarmHistoryUseCase; +import com.moing.backend.domain.history.application.service.ReadAlarmHistoryUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.moing.backend.domain.history.presentation.constant.AlarmHistoryResponseMessage.*; + + +@RestController +@AllArgsConstructor +@RequestMapping("/api/history/alarm") +public class AlarmHistoryController { + + private final GetAlarmHistoryUseCase getAlarmHistoryUseCase; + private final ReadAlarmHistoryUseCase readAlarmHistoryUseCase; + + /** + * 알림 전체 조회 + * [GET] api/history/alarm + * 작성자 : 김민수 + */ + @GetMapping + public ResponseEntity>> getAllAlarmHistories(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_ALL_ALARM_HISTORY.getMessage(), getAlarmHistoryUseCase.getAllAlarmHistories(user.getSocialId()))); + } + + /** + * 알림 한개 조회 + * [POST] api/history/alarm/read?alarmHistoryId=1 + */ + @PostMapping("/read") + public ResponseEntity readAlarmHistory(@AuthenticationPrincipal User user, + @RequestParam Long alarmHistoryId) { + readAlarmHistoryUseCase.readAlarmHistory(user.getSocialId(), alarmHistoryId); + return ResponseEntity.ok(SuccessResponse.create(READ_ALARM_HISTORY.getMessage())); + } + + /** + * 안 읽은 알림 개수 조회 + * [GET] api/history/alarm/count + */ + @GetMapping("/count") + public ResponseEntity> getUnreadAlarmCount(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_UNREAD_ALARM_HISTORY.getMessage(), getAlarmHistoryUseCase.getUnreadAlarmCount(user.getSocialId()))); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/history/presentation/constant/AlarmHistoryResponseMessage.java b/src/main/java/com/moing/backend/domain/history/presentation/constant/AlarmHistoryResponseMessage.java new file mode 100644 index 00000000..8781c3a0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/history/presentation/constant/AlarmHistoryResponseMessage.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.history.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AlarmHistoryResponseMessage { + GET_ALL_ALARM_HISTORY("알림 히스토리를 모두 조회했습니다."), + READ_ALARM_HISTORY("알림 히스토리 한 개를 조회했습니다."), + GET_UNREAD_ALARM_HISTORY("안읽은 알림 개수를 조회했습니다"); + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/application/dto/ImageFileExtension.java b/src/main/java/com/moing/backend/domain/infra/image/application/dto/ImageFileExtension.java new file mode 100644 index 00000000..b93a1bdc --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/application/dto/ImageFileExtension.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.infra.image.application.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ImageFileExtension { + JPEG("jpeg"), + JPG("jpg"), + PNG("png"); + + private final String uploadExtension; +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/application/dto/ImageUrlDto.java b/src/main/java/com/moing/backend/domain/infra/image/application/dto/ImageUrlDto.java new file mode 100644 index 00000000..8b96e15a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/application/dto/ImageUrlDto.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.infra.image.application.dto; + + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ImageUrlDto { + + private final String presignedUrl; + private final String key; + + public static ImageUrlDto of(String url, String key) { + return ImageUrlDto.builder().presignedUrl(url).key(key).build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/application/dto/request/IssuePresignedUrlRequest.java b/src/main/java/com/moing/backend/domain/infra/image/application/dto/request/IssuePresignedUrlRequest.java new file mode 100644 index 00000000..f123d803 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/application/dto/request/IssuePresignedUrlRequest.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.infra.image.application.dto.request; + + +import com.moing.backend.domain.infra.image.application.dto.ImageFileExtension; +import com.moing.backend.global.annotation.ValidEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class IssuePresignedUrlRequest { + + + @NotBlank(message = "파일 확장자를 입력해주세요.") + @ValidEnum( + enumClass = ImageFileExtension.class, + message = "유효하지 않은 ImageFileExtension 파라미터입니다.") + private ImageFileExtension imageFileExtension; + +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/application/dto/response/IssuePresignedUrlResponse.java b/src/main/java/com/moing/backend/domain/infra/image/application/dto/response/IssuePresignedUrlResponse.java new file mode 100644 index 00000000..4b01271c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/application/dto/response/IssuePresignedUrlResponse.java @@ -0,0 +1,25 @@ +package com.moing.backend.domain.infra.image.application.dto.response; + + +import com.moing.backend.domain.infra.image.application.dto.ImageUrlDto; +import com.moing.backend.global.config.s3.ImageUrlUtil; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class IssuePresignedUrlResponse { + + private final String presignedUrl; + + private final String imgUrl; + + public static IssuePresignedUrlResponse from(ImageUrlDto urlDto) { + String imgUrl = ImageUrlUtil.prefix +"/"+ urlDto.getKey(); + + return IssuePresignedUrlResponse.builder() + .presignedUrl(urlDto.getPresignedUrl()) + .imgUrl(imgUrl) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/application/service/IssuePresignedUrlUseCase.java b/src/main/java/com/moing/backend/domain/infra/image/application/service/IssuePresignedUrlUseCase.java new file mode 100644 index 00000000..b95af55c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/application/service/IssuePresignedUrlUseCase.java @@ -0,0 +1,23 @@ +package com.moing.backend.domain.infra.image.application.service; + + +import com.moing.backend.domain.infra.image.application.dto.request.IssuePresignedUrlRequest; +import com.moing.backend.domain.infra.image.application.dto.response.IssuePresignedUrlResponse; +import com.moing.backend.global.config.s3.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +public class IssuePresignedUrlUseCase { + + private final S3Service s3Service; + + public IssuePresignedUrlResponse execute(IssuePresignedUrlRequest issuePresignedUrlRequest) { + + return IssuePresignedUrlResponse.from( + s3Service.issuePreSignedUrl( + issuePresignedUrlRequest.getImageFileExtension())); + } +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/presentation/ImageController.java b/src/main/java/com/moing/backend/domain/infra/image/presentation/ImageController.java new file mode 100644 index 00000000..6499a141 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/presentation/ImageController.java @@ -0,0 +1,29 @@ +package com.moing.backend.domain.infra.image.presentation; + +import com.moing.backend.domain.infra.image.application.dto.request.IssuePresignedUrlRequest; +import com.moing.backend.domain.infra.image.application.dto.response.IssuePresignedUrlResponse; +import com.moing.backend.domain.infra.image.application.service.IssuePresignedUrlUseCase; +import com.moing.backend.domain.infra.image.presentation.constant.EImageResponseMessage; +import com.moing.backend.global.response.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/api/image") +@RequiredArgsConstructor +public class ImageController { + + private final IssuePresignedUrlUseCase getPresignedUrlUseCase; + + + @PostMapping("/presigned") + public ResponseEntity> createPresigned(@RequestBody IssuePresignedUrlRequest issuePresignedUrlRequest) { + + return ResponseEntity.ok(SuccessResponse.create(EImageResponseMessage.ISSUE_PRESIGNED_URL_SUCCESS.getMessage(), getPresignedUrlUseCase.execute(issuePresignedUrlRequest))); + } +} diff --git a/src/main/java/com/moing/backend/domain/infra/image/presentation/constant/EImageResponseMessage.java b/src/main/java/com/moing/backend/domain/infra/image/presentation/constant/EImageResponseMessage.java new file mode 100644 index 00000000..be3e14c0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/infra/image/presentation/constant/EImageResponseMessage.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.infra.image.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EImageResponseMessage { + ISSUE_PRESIGNED_URL_SUCCESS("presignedUrl을 발급하였습니다"); + + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/member/application/mapper/MemberMapper.java b/src/main/java/com/moing/backend/domain/member/application/mapper/MemberMapper.java new file mode 100644 index 00000000..8b20e694 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/application/mapper/MemberMapper.java @@ -0,0 +1,46 @@ +package com.moing.backend.domain.member.application.mapper; + +import com.moing.backend.domain.auth.application.dto.response.GoogleUserResponse; +import com.moing.backend.domain.auth.application.dto.response.KakaoUserResponse; +import com.moing.backend.domain.member.domain.constant.RegistrationStatus; +import com.moing.backend.domain.member.domain.constant.Role; +import com.moing.backend.domain.member.domain.constant.SocialProvider; +import com.moing.backend.domain.member.domain.entity.Member; +import org.springframework.stereotype.Component; + +@Component +public class MemberMapper { + + public static Member createKakaoMember(KakaoUserResponse kakaoUserResponse) { + + return Member.builder() + .socialId(SocialProvider.KAKAO + "@" + kakaoUserResponse.getId()) + .provider(SocialProvider.KAKAO) + .email(kakaoUserResponse.getKakaoAccount().getEmail()) + .role(Role.USER) + .registrationStatus(RegistrationStatus.UNCOMPLETED) + .build(); + } + + public static Member createAppleMember(String socialId, String email) { + + return Member.builder() + .socialId(SocialProvider.APPLE + "@" + socialId) + .provider(SocialProvider.APPLE) + .email(email) + .role(Role.USER) + .registrationStatus(RegistrationStatus.UNCOMPLETED) + .build(); + } + + public static Member createGoogleMember(GoogleUserResponse googleUserResponse){ + + return Member.builder() + .socialId(SocialProvider.GOOGLE + "@" + googleUserResponse.getSub()) + .provider(SocialProvider.GOOGLE) + .email(googleUserResponse.getEmail()) + .role(Role.USER) + .registrationStatus(RegistrationStatus.UNCOMPLETED) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/member/application/service/UpdateRemindAlarmUseCase.java b/src/main/java/com/moing/backend/domain/member/application/service/UpdateRemindAlarmUseCase.java new file mode 100644 index 00000000..d4203f0d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/application/service/UpdateRemindAlarmUseCase.java @@ -0,0 +1,91 @@ +package com.moing.backend.domain.member.application.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.application.service.SaveMultiAlarmHistoryUseCase; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.application.service.SendMissionStartAlarmUseCase; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.global.config.fcm.dto.request.MultiRequest; +import com.moing.backend.global.config.fcm.service.MultiMessageSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.moing.backend.global.config.fcm.constant.RemindMissionTitle.REMIND_ON_MONDAY_MESSAGE; +import static com.moing.backend.global.config.fcm.constant.RemindMissionTitle.REMIND_ON_MONDAY_TITLE; +@Service +@Transactional +@RequiredArgsConstructor +public class UpdateRemindAlarmUseCase { + + private final MemberGetService memberGetService; + private final SaveMultiAlarmHistoryUseCase saveMultiAlarmHistoryUseCase; + private final MultiMessageSender multiMessageSender; + + String REMIND_NAME = "미션 리마인드"; + + + public void sendUpdateAppPushAlarm(String title, String message) { + +// String title = "MOING 업데이트 소식"; +// String message = "이제 누구나 미션을 만들 수 있어요. 지금 업데이트하고 다른 소식도 확인해보세요!"; + + long count = memberGetService.getAllMemberOfPushAlarm(0L, Long.MAX_VALUE).size(); + + for (Long offset = 0L; offset < count; offset += 499) { + + Long limit = offset+499; + + List allMemberOfPushAlarm = memberGetService.getAllMemberOfPushAlarm(offset, limit); + + Optional> memberIdAndTokens = mapToMemberAndToken(allMemberOfPushAlarm); + Optional> pushMemberIdAndToken = isPushMemberIdAndToken(allMemberOfPushAlarm); + + if (pushMemberIdAndToken.isPresent() && !pushMemberIdAndToken.get().isEmpty()) { + multiMessageSender.send(new MultiRequest(pushMemberIdAndToken.get(), title, message, "", REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue())); + } + if (memberIdAndTokens.isPresent() && !memberIdAndTokens.get().isEmpty()) { + saveMultiAlarmHistoryUseCase.saveAlarmHistories(AlarmHistoryMapper.getMemberIds(memberIdAndTokens.get()), "", title, message, REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue()); + } + } + + + } + + public Optional> mapToMemberAndToken(List members) { + return Optional.of(members.stream() + .map(member -> MemberIdAndToken.builder() + .fcmToken(member.getFcmToken()) + .memberId(member.getMemberId()) + .build()) + .collect(Collectors.toList())); + } + public Optional> isPushMemberIdAndToken(List members) { + return Optional.of(members.stream() + .map(member -> { + if (member.isRemindPush() && !member.isSignOut()) { + return MemberIdAndToken.builder() + .fcmToken(member.getFcmToken()) + .memberId(member.getMemberId()) + .build(); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + } + +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/constant/Gender.java b/src/main/java/com/moing/backend/domain/member/domain/constant/Gender.java new file mode 100644 index 00000000..e66c7b8f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/constant/Gender.java @@ -0,0 +1,10 @@ +package com.moing.backend.domain.member.domain.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Gender { + MAN, WOMAN, NEUTRALITY +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/constant/RegistrationStatus.java b/src/main/java/com/moing/backend/domain/member/domain/constant/RegistrationStatus.java new file mode 100644 index 00000000..22b747fa --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/constant/RegistrationStatus.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.member.domain.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum RegistrationStatus { + UNCOMPLETED("회원가입을 합니다. 추가 정보를 입력하세요."), + COMPLETED("로그인을 합니다."); + private String message; +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/constant/Role.java b/src/main/java/com/moing/backend/domain/member/domain/constant/Role.java new file mode 100644 index 00000000..d4ac6927 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/constant/Role.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.member.domain.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Role { + USER("ROLE_USER"), + ADMIN("ROLE_ADMIN"); + + private final String key; +} + diff --git a/src/main/java/com/moing/backend/domain/member/domain/constant/SocialProvider.java b/src/main/java/com/moing/backend/domain/member/domain/constant/SocialProvider.java new file mode 100644 index 00000000..8779d851 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/constant/SocialProvider.java @@ -0,0 +1,10 @@ +package com.moing.backend.domain.member.domain.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum SocialProvider { + GOOGLE, APPLE, KAKAO; +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/entity/Member.java b/src/main/java/com/moing/backend/domain/member/domain/entity/Member.java new file mode 100644 index 00000000..206847e1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/entity/Member.java @@ -0,0 +1,192 @@ +package com.moing.backend.domain.member.domain.entity; + +import com.moing.backend.domain.auth.application.dto.request.SignUpRequest; +import com.moing.backend.domain.member.domain.constant.Gender; +import com.moing.backend.domain.member.domain.constant.RegistrationStatus; +import com.moing.backend.domain.member.domain.constant.Role; +import com.moing.backend.domain.member.domain.constant.SocialProvider; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.global.entity.BaseTimeEntity; +import com.moing.backend.global.utils.AesConverter; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import javax.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Table(indexes = @Index(name = "idx_social_id", columnList = "socialId")) +public class Member extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long memberId; + + @Column(nullable = false) + private String socialId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SocialProvider provider; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RegistrationStatus registrationStatus; + + @Convert(converter = AesConverter.class) + private String email; + + private String profileImage; + + @Column(length = 10) + @Enumerated(EnumType.STRING) + private Gender gender; + + private LocalDate birthDate; + + @Convert(converter = AesConverter.class) + private String nickName; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Role role; + + private String introduction; + + private String fcmToken; + + @ColumnDefault("true") + @Column(nullable = false) + private boolean isNewUploadPush; + + @ColumnDefault("true") + @Column(nullable = false) + private boolean isRemindPush; + + @ColumnDefault("true") + @Column(nullable = false) + private boolean isFirePush; + + @ColumnDefault("true") + @Column(nullable = false) + private boolean isCommentPush; + + private boolean isDeleted; + + private LocalDateTime lastSignInTime; + + private boolean isSignOut; + + @OneToMany(mappedBy = "member") + private List teamMembers = new ArrayList<>(); //최대 3개이므로 양방향 + + //==생성 메서드==// + public static Member valueOf(OAuth2User oAuth2User) { + var attributes = oAuth2User.getAttributes(); + return Member.builder() + .socialId((String) attributes.get("socialId")) + .provider((SocialProvider) attributes.get("provider")) + .nickName((String) attributes.get("nickname")) + .email((String) attributes.get("email")) + .role((Role) attributes.get("role")) + .registrationStatus(RegistrationStatus.UNCOMPLETED) + .build(); + } + + public void signUp(SignUpRequest signUpRequest) { + this.nickName = signUpRequest.getNickName(); + if(signUpRequest.getGender()!=null) { + this.gender = signUpRequest.getGender(); + } + if(signUpRequest.getBirthDate()!=null) { + this.birthDate = LocalDate.parse(signUpRequest.getBirthDate(), DateTimeFormatter.ISO_DATE); + } + this.registrationStatus = RegistrationStatus.COMPLETED; + updateAllPush(true); + } + + @Builder + public Member(String email, String profileImage, Gender gender, LocalDate birthDate, Role role) { + this.email = email; + this.profileImage = profileImage; + this.gender = gender; + this.birthDate = birthDate; + this.role = role; + } + + public void updateProfile(String profileImage, String nickName, String introduction) { + this.profileImage = profileImage; + this.nickName = nickName; + this.introduction = introduction; + } + + public void updateNewUploadPush(boolean newUploadPush) { + this.isNewUploadPush = newUploadPush; + } + + public void updateRemindPush(boolean remindPush) { + this.isRemindPush = remindPush; + } + + public void updateFirePush(boolean firePush) { + this.isFirePush = firePush; + } + + public void updateCommentPush(boolean commentPush){ + this.isCommentPush=commentPush; + } + + public void updateAllPush(boolean allPush) { + this.isNewUploadPush = allPush; + this.isRemindPush = allPush; + this.isFirePush = allPush; + this.isCommentPush=allPush; + } + + public void updateFcmToken(String fcmToken) { + this.fcmToken = fcmToken; + } + + public void updateLastSignInTime(LocalDateTime time){ + this.lastSignInTime=time; + } + + public Member(LocalDate birthDate, String email, String fcmToken, Gender gender, String introduction, String nickName, String profileImage, SocialProvider provider, RegistrationStatus registrationStatus, Role role, String socialId) { + this.birthDate = birthDate; + this.email = email; + this.fcmToken = fcmToken; + this.gender = gender; + this.introduction = introduction; + this.nickName = nickName; + this.profileImage = profileImage; + this.provider = provider; + this.registrationStatus = registrationStatus; + this.role = role; + this.socialId = socialId; + } + + public void deleteMember(){ + this.isDeleted=true; + } + + public void signOut() { + this.isSignOut = true; + } + + public void signIn() { + this.isSignOut = false; + } + +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java new file mode 100644 index 00000000..b6b2f76e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepository.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.member.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; + +import java.util.List; +import java.util.Optional; + +public interface MemberCustomRepository { + + boolean checkNickname(String nickname); + Optional findNotDeletedBySocialId(String socialId); + Optional findNotDeletedByEmail(String email); + Optional findNotDeletedByMemberId(Long id); + Long getTodayNewMembers(); + Long getYesterdayNewMembers(); + + Optional> findAllMemberOnPushAlarm(Long offset, Long limit); +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java new file mode 100644 index 00000000..1fa3873a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryImpl.java @@ -0,0 +1,106 @@ +package com.moing.backend.domain.member.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.member.domain.entity.QMember.member; + +public class MemberCustomRepositoryImpl implements MemberCustomRepository { + + private final JPAQueryFactory queryFactory; + + public MemberCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public boolean checkNickname(String nickname) { + return queryFactory + .selectOne() + .from(member) + .where(member.nickName.eq(nickname)) + .where(member.isDeleted.eq(false)) + .fetchFirst() != null; + } + + @Override + public Optional findNotDeletedBySocialId(String socialId) { + return Optional.ofNullable(queryFactory + .selectFrom(member) + .where(member.socialId.eq(socialId)) + .where(member.isDeleted.eq(false)) + .fetchOne()); + } + + @Override + public Optional findNotDeletedByEmail(String email) { + return Optional.ofNullable(queryFactory + .selectFrom(member) + .where(member.email.eq(email)) + .where(member.isDeleted.eq(false)) + .fetchOne()); + } + + @Override + public Optional findNotDeletedByMemberId(Long id) { + return Optional.ofNullable(queryFactory + .selectFrom(member) + .where(member.memberId.eq(id)) + .where(member.isDeleted.eq(false)) + .fetchOne()); + } + + @Override + public Long getTodayNewMembers() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long todayNewMembers = queryFactory + .selectFrom(member) + .where(member.createdDate.between(startOfToday, endOfToday)) + .fetchCount(); + + return todayNewMembers; + } + + @Override + public Long getYesterdayNewMembers() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + + long yesterdayNewMembers = queryFactory + .selectFrom(member) + .where(member.createdDate.between(startOfYesterday, startOfToday)) + .fetchCount(); + + return yesterdayNewMembers; + } + + @Override + public Optional> findAllMemberOnPushAlarm(Long offset, Long limit) { + return Optional.ofNullable( + queryFactory.selectFrom(member) + .where( + member.isDeleted.eq(false), + member.isRemindPush.eq(true), + member.isSignOut.eq(false) + ) + .offset(offset) + .limit(limit) + .fetch() + ); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java new file mode 100644 index 00000000..c797cf41 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/repository/MemberRepository.java @@ -0,0 +1,8 @@ +package com.moing.backend.domain.member.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository, MemberCustomRepository { + +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/service/MemberCheckService.java b/src/main/java/com/moing/backend/domain/member/domain/service/MemberCheckService.java new file mode 100644 index 00000000..226a10c0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/service/MemberCheckService.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.member.domain.service; + +import com.moing.backend.domain.member.domain.repository.MemberRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@RequiredArgsConstructor +public class MemberCheckService { + private final MemberRepository memberRepository; + + @Transactional(readOnly = true) + public boolean checkNickname(String nickname) { + return memberRepository.checkNickname(nickname); + } + +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/service/MemberDeleteService.java b/src/main/java/com/moing/backend/domain/member/domain/service/MemberDeleteService.java new file mode 100644 index 00000000..843fcde5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/service/MemberDeleteService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.member.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.repository.MemberRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MemberDeleteService { + + private final MemberRepository memberRepository; + + public void deleteMember(Member member){ + this.memberRepository.delete(member); + } +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java b/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java new file mode 100644 index 00000000..36013dce --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/service/MemberGetService.java @@ -0,0 +1,39 @@ +package com.moing.backend.domain.member.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.repository.MemberRepository; +import com.moing.backend.domain.member.exception.NotFoundBySocialIdException; +import com.moing.backend.domain.member.exception.NotFoundRemindAlarmException; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.List; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MemberGetService { + private final MemberRepository memberRepository; + + public Member getMemberBySocialId(String socialId){ + return memberRepository.findNotDeletedBySocialId(socialId).orElseThrow(()->new NotFoundBySocialIdException()); + } + + public Member getMemberByMemberId(Long memberId) { + return memberRepository.findNotDeletedByMemberId(memberId).orElseThrow(()->new NotFoundBySocialIdException()); + } + + public Long getTodayNewMembers(){ + return memberRepository.getTodayNewMembers(); + } + + public Long getYesterdayNewMembers(){ + return memberRepository.getYesterdayNewMembers(); + } + + + public List getAllMemberOfPushAlarm(Long offset, Long limit) { + return memberRepository.findAllMemberOnPushAlarm(offset,limit).orElseThrow(NotFoundRemindAlarmException::new); + } +} diff --git a/src/main/java/com/moing/backend/domain/member/domain/service/MemberSaveService.java b/src/main/java/com/moing/backend/domain/member/domain/service/MemberSaveService.java new file mode 100644 index 00000000..ad1906b9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/domain/service/MemberSaveService.java @@ -0,0 +1,30 @@ +package com.moing.backend.domain.member.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.repository.MemberRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.AllArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +@DomainService +@AllArgsConstructor +public class MemberSaveService { + + private final MemberRepository memberRepository; + + @Transactional + public Member saveMember(Member member) { + OptionalfindMember=memberRepository.findNotDeletedBySocialId(member.getSocialId()); + if(findMember.isEmpty()){ + return memberRepository.save(member); + } else { + findMember.get().updateFcmToken(member.getFcmToken()); + findMember.get().updateLastSignInTime(LocalDateTime.now()); + findMember.get().signIn(); + return findMember.get(); + } + } +} diff --git a/src/main/java/com/moing/backend/domain/member/dto/request/PostUpdatePushAlarm.java b/src/main/java/com/moing/backend/domain/member/dto/request/PostUpdatePushAlarm.java new file mode 100644 index 00000000..bc5d0fe6 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/dto/request/PostUpdatePushAlarm.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.member.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class PostUpdatePushAlarm { + + private String title; + private String message; +} diff --git a/src/main/java/com/moing/backend/domain/member/dto/response/UserProperty.java b/src/main/java/com/moing/backend/domain/member/dto/response/UserProperty.java new file mode 100644 index 00000000..14eaa609 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/dto/response/UserProperty.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.member.dto.response; + +import com.moing.backend.domain.member.domain.constant.Gender; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class UserProperty { + private Gender gender; + private LocalDate birthDate; +} diff --git a/src/main/java/com/moing/backend/domain/member/exception/MemberException.java b/src/main/java/com/moing/backend/domain/member/exception/MemberException.java new file mode 100644 index 00000000..7df8cf88 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/exception/MemberException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.member.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class MemberException extends ApplicationException { + protected MemberException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/domain/member/exception/NotFoundBySocialIdException.java b/src/main/java/com/moing/backend/domain/member/exception/NotFoundBySocialIdException.java new file mode 100644 index 00000000..4b3f3fd4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/exception/NotFoundBySocialIdException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.member.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundBySocialIdException extends MemberException { + public NotFoundBySocialIdException() { + super(ErrorCode.NOT_FOUND_BY_SOCIAL_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/member/exception/NotFoundRemindAlarmException.java b/src/main/java/com/moing/backend/domain/member/exception/NotFoundRemindAlarmException.java new file mode 100644 index 00000000..679e8d2f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/exception/NotFoundRemindAlarmException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.member.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundRemindAlarmException extends MemberException { + public NotFoundRemindAlarmException() { + super(ErrorCode.NOT_FOUND_ALL_MEMBER, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/member/presentation/RemindAlarmController.java b/src/main/java/com/moing/backend/domain/member/presentation/RemindAlarmController.java new file mode 100644 index 00000000..799d7437 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/member/presentation/RemindAlarmController.java @@ -0,0 +1,29 @@ +package com.moing.backend.domain.member.presentation; + +import com.moing.backend.domain.member.application.service.UpdateRemindAlarmUseCase; +import com.moing.backend.domain.member.dto.request.PostUpdatePushAlarm; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/remind/alarm") +public class RemindAlarmController { + + private final UpdateRemindAlarmUseCase updateRemindAlarmUseCase; + + + @PostMapping("/update") + public ResponseEntity> postUpdateRemindAlarm(@AuthenticationPrincipal User user , @RequestBody PostUpdatePushAlarm postUpdatePushAlarm) { + updateRemindAlarmUseCase.sendUpdateAppPushAlarm(postUpdatePushAlarm.getTitle(), postUpdatePushAlarm.getMessage()); + return ResponseEntity.ok(SuccessResponse.create("remindAlarm Done")); + } + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/req/MissionReq.java b/src/main/java/com/moing/backend/domain/mission/application/dto/req/MissionReq.java new file mode 100644 index 00000000..520a41a5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/req/MissionReq.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.mission.application.dto.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.*; + + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class MissionReq { + + private String title; + private String dueTo; + + private String rule; + private String content; + private int number; + + private String type; + private String way; + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/FinishMissionBoardRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/FinishMissionBoardRes.java new file mode 100644 index 00000000..824246b7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/FinishMissionBoardRes.java @@ -0,0 +1,32 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchiveStatus; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Builder +@Getter +@Setter +public class FinishMissionBoardRes { + private Long missionId; + private String dueTo; // 날짜 + private String title; + private String status; + private String missionType; + private String missionWay; + + public FinishMissionBoardRes(Long missionId, String dueTo, String title, String status, String missionType,String missionWay) { + this.missionId = missionId; + this.dueTo = dueTo; + this.title = title; + this.status = status; + this.missionType = missionType; + this.missionWay = missionWay; + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/GatherRepeatMissionRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/GatherRepeatMissionRes.java new file mode 100644 index 00000000..1859d7d1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/GatherRepeatMissionRes.java @@ -0,0 +1,25 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class GatherRepeatMissionRes { + + private Long missionId; + private Long teamId; + private String teamName; + private String missionTitle; + private String totalNum; + private String doneNum; + private String status; + private String donePeople; + private String totalPeople; + + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/GatherSingleMissionRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/GatherSingleMissionRes.java new file mode 100644 index 00000000..27b6a46e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/GatherSingleMissionRes.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@Builder +@ToString +@AllArgsConstructor +public class GatherSingleMissionRes { + private Long missionId; + private Long teamId; + private String teamName; + private String missionTitle; + private String dueTo; + private String status; + private String done; + private String total; + + public GatherSingleMissionRes(Long missionId, Long teamId, String teamName, String missionTitle, String dueTo, String status, String total) { + this.missionId = missionId; + this.teamId = teamId; + this.teamName = teamName; + this.missionTitle = missionTitle; + this.dueTo = dueTo; + this.status = status; + this.total = total; + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionConfirmRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionConfirmRes.java new file mode 100644 index 00000000..91530ec9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionConfirmRes.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class MissionConfirmRes { + private Long missionId; +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionCreateRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionCreateRes.java new file mode 100644 index 00000000..39d07123 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionCreateRes.java @@ -0,0 +1,25 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MissionCreateRes { + + private Long missionId; + + private String title; + private String dueTo; + + private String rule; + private String content; + private int number; + + private String type; + private String status; + private String way; + + private Boolean isLeader; + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionReadRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionReadRes.java new file mode 100644 index 00000000..22aeac19 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionReadRes.java @@ -0,0 +1,23 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class MissionReadRes { + + private String title; + private String dueTo; + + private String rule; + private String content; + + private String type; + private String way; + + private Boolean isLeader; + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionRecommendRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionRecommendRes.java new file mode 100644 index 00000000..33663c13 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/MissionRecommendRes.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class MissionRecommendRes { + String category; +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/RepeatMissionBoardRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/RepeatMissionBoardRes.java new file mode 100644 index 00000000..e6ba3d27 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/RepeatMissionBoardRes.java @@ -0,0 +1,54 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class RepeatMissionBoardRes { + private Long missionId; + private String title; + + private String dueTo; // 요일 상태 넘겨주기 +// private String status; + private Long done; + private int number; + private String way; + private String status; + private Boolean isRead; + + + public RepeatMissionBoardRes(Long missionId, String title, Long done,int number,String way,String status, Boolean isRead) { + this.missionId = missionId; + this.title = title; + this.dueTo="False"; + this.number = number; + this.done = done; + this.way = way; + this.status = status; + this.isRead=isRead; + } + + public RepeatMissionBoardRes(Long missionId, String title, String dueTo, Long done, int number,String way) { + this.missionId = missionId; + this.title = title; + this.dueTo = "False"; + this.done = done; + this.number = number; + this.way = way; + } + + @Builder + public RepeatMissionBoardRes(Long missionId, String title, String dueTo, Long done, int number, String way, String status, Boolean isRead) { + this.missionId = missionId; + this.title = title; + this.dueTo = dueTo; + this.done = done; + this.number = number; + this.way = way; + this.status = status; + this.isRead=isRead; + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/dto/res/SingleMissionBoardRes.java b/src/main/java/com/moing/backend/domain/mission/application/dto/res/SingleMissionBoardRes.java new file mode 100644 index 00000000..f5978531 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/dto/res/SingleMissionBoardRes.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.mission.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class SingleMissionBoardRes { + private Long missionId; + private String dueTo; // 날짜 + private String title; + private String status; + private String missionType; + private Boolean isRead; + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/mapper/MissionMapper.java b/src/main/java/com/moing/backend/domain/mission/application/mapper/MissionMapper.java new file mode 100644 index 00000000..a6ca5266 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/mapper/MissionMapper.java @@ -0,0 +1,67 @@ +package com.moing.backend.domain.mission.application.mapper; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.req.MissionReq; +import com.moing.backend.domain.mission.application.dto.res.MissionCreateRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.annotation.Mapper; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Mapper +public class MissionMapper { + + public static Mission mapToMission(MissionReq missionReq, Member member, MissionStatus status) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + return Mission.builder() + .title(missionReq.getTitle()) + .dueTo(LocalDateTime.parse(missionReq.getDueTo(), formatter)) + .rule(missionReq.getRule()) + .content(missionReq.getContent()) + .way(MissionWay.valueOf(missionReq.getWay())) + .type(MissionType.valueOf(missionReq.getType())) + .number(missionReq.getNumber()) + .status(status) + .makerId(member.getMemberId()) + .build(); + + } + + public static MissionCreateRes mapToMissionCreateRes(Mission mission,Member member) { + + + return MissionCreateRes.builder() + .missionId(mission.getId()) + .title(mission.getTitle()) + .dueTo(mission.getDueTo().toString()) + .rule(mission.getRule()) + .way(mission.getWay().name()) + .content(mission.getContent()) + .type(mission.getType().name()) + .status(mission.getStatus().name()) + .number(mission.getNumber()) + .isLeader( mission.getMakerId().equals(member.getMemberId()) || mission.getTeam().getLeaderId().equals(member.getMemberId())) + .build(); + } + + public static MissionReadRes mapToMissionReadRes(Mission mission,Member member) { + + return MissionReadRes.builder() + .title(mission.getTitle()) + .dueTo(mission.getDueTo().toString()) + .rule(mission.getRule()) + .way(mission.getWay().name()) + .content(mission.getContent()) + .type(mission.getType().name()) + .isLeader( mission.getMakerId().equals(member.getMemberId()) || mission.getTeam().getLeaderId().equals(member.getMemberId())) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionCreateUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionCreateUseCase.java new file mode 100644 index 00000000..c4d223ec --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionCreateUseCase.java @@ -0,0 +1,80 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.application.dto.req.MissionReq; +import com.moing.backend.domain.mission.application.dto.res.MissionCreateRes; +import com.moing.backend.domain.mission.application.dto.res.MissionRecommendRes; +import com.moing.backend.domain.mission.application.mapper.MissionMapper; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.mission.domain.service.MissionSaveService; +import com.moing.backend.domain.mission.exception.NoAccessCreateMission; +import com.moing.backend.domain.mission.exception.NoMoreCreateMission; +import com.moing.backend.domain.missionRead.application.service.CreateMissionReadUseCase; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.global.response.BaseServiceResponse; +import com.moing.backend.global.utils.BaseService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionCreateUseCase { + + private final MissionSaveService missionSaveService; + private final MissionQueryService missionQueryService; + private final SendMissionCreateAlarmUseCase sendMissionCreateAlarmUseCase; + private final CreateMissionReadUseCase createMissionReadUseCase; + + private final BaseService baseService; + + public MissionCreateRes createMission(String socialId, Long teamId, MissionReq missionReq) { + + BaseServiceResponse commonData = baseService.getCommonData(socialId, teamId); + Member member = commonData.getMember(); + Team team = commonData.getTeam(); + + Mission mission = MissionMapper.mapToMission(missionReq, member, MissionStatus.WAIT); + mission.setTeam(team); + + if (mission.getType().equals(MissionType.REPEAT)) { + // 반복 미션 생성일 경우 소모임장만 가능 + if (!(member.getMemberId().equals(team.getLeaderId()))) { + throw new NoAccessCreateMission(); + // 반복미션은 최대 2개 생성 가능 + } else if (missionQueryService.isAbleCreateRepeatMission(team.getTeamId())) { + throw new NoMoreCreateMission(); + } + // 반복미션 유예 해제 + mission.updateStatus(MissionStatus.ONGOING); + + } + // 2. 미션 저장 + missionSaveService.save(mission); + + // 3. 미션 읽음 처리 + createMissionReadUseCase.createMissionRead(team,member, mission); + + // 4. 알림 보내기 - 미션 생성 + sendMissionCreateAlarmUseCase.sendNewMissionUploadAlarm(member, mission); + + return MissionMapper.mapToMissionCreateRes(mission,member); + } + + + public Boolean getIsLeader(String socialId, Long teamId) { + BaseServiceResponse commonData = baseService.getCommonData(socialId, teamId); + Member member = commonData.getMember(); + Team team = commonData.getTeam(); + + return member.getMemberId().equals(team.getLeaderId()); + + } + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionDeleteUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionDeleteUseCase.java new file mode 100644 index 00000000..2a6479f6 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionDeleteUseCase.java @@ -0,0 +1,44 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.repository.MissionRepository; +import com.moing.backend.domain.mission.domain.service.MissionDeleteService; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.mission.exception.NoAccessCreateMission; +import com.moing.backend.domain.mission.exception.NoAccessDeleteMission; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.response.BaseServiceResponse; +import com.moing.backend.global.utils.BaseService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionDeleteUseCase { + + private final MissionDeleteService missionDeleteService; + private final MemberGetService memberGetService; + + private final BaseService baseService; + private final MissionQueryService missionQueryService; + + public Long deleteMission(String userSocialId,Long missionId) { + + + Member member = memberGetService.getMemberBySocialId(userSocialId); + Mission mission = missionQueryService.findMissionById(missionId); + Team team = mission.getTeam(); + + Long memberId = member.getMemberId(); + + if (!memberId.equals(mission.getMakerId()) || memberId.equals(team.getLeaderId())) { + throw new NoAccessDeleteMission(); + } + return missionDeleteService.deleteMission(missionId); + } + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionGatherBoardUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionGatherBoardUseCase.java new file mode 100644 index 00000000..0b502fea --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionGatherBoardUseCase.java @@ -0,0 +1,71 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.application.dto.res.GatherRepeatMissionRes; +import com.moing.backend.domain.mission.application.dto.res.GatherSingleMissionRes; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchivePhotoRes; +import com.moing.backend.domain.missionArchive.application.dto.res.MyTeamsRes; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionGatherBoardUseCase { + + private final MissionQueryService missionQueryService; + private final MissionArchiveQueryService missionArchiveQueryService; + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + + public List getAllActiveSingleMissions(String userId) { + Long memberId = memberGetService.getMemberBySocialId(userId).getMemberId(); + return missionQueryService.findAllSingleMission(memberId); + + } + public List getAllActiveRepeatMissions(String userId) { + Long memberId = memberGetService.getMemberBySocialId(userId).getMemberId(); + return missionQueryService.findAllRepeatMission(memberId); + + } + + public List getTeamActiveSingleMissions(String userId,Long teamId) { + Long memberId = memberGetService.getMemberBySocialId(userId).getMemberId(); + return missionQueryService.findTeamSingleMission(memberId,teamId); + + } + public List getTeamActiveRepeatMissions(String userId,Long teamId) { + Long memberId = memberGetService.getMemberBySocialId(userId).getMemberId(); + return missionQueryService.findTeamRepeatMission(memberId,teamId); + + } + + public List getArchivePhotoByTeamRes(String userId) { + Long memberId = memberGetService.getMemberBySocialId(userId).getMemberId(); + + List teamIdByMemberId = teamGetService.getTeamIdByMemberId(memberId); + + return missionArchiveQueryService.findTop5ArchivesByTeam(teamIdByMemberId); + } + + + public List getMyTeams(String userId) { + Long memberId = memberGetService.getMemberBySocialId(userId).getMemberId(); + List teamIdByMemberId = teamGetService.getTeamIdByMemberId(memberId); + + return teamGetService.getTeamNameByTeamId(teamIdByMemberId); + + } + + + + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionReadUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionReadUseCase.java new file mode 100644 index 00000000..b854964a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionReadUseCase.java @@ -0,0 +1,44 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.application.dto.res.MissionConfirmRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionRead.application.service.CreateMissionReadUseCase; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionReadUseCase { + + private final MissionQueryService missionQueryService; + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final CreateMissionReadUseCase createMissionReadUseCase; + + public MissionReadRes getMission(String userSocialId, Long missionId) { + + Member member = memberGetService.getMemberBySocialId(userSocialId); + return missionQueryService.findMissionByIds(member.getMemberId(),missionId); + } + + public String getTeamCategory(Long teamId) { + Team team = teamGetService.getTeamByTeamId(teamId); + return team.getCategory(); + } + + public MissionConfirmRes confirmMission(String socialId, Long missionId, Long teamId){ + Member member=memberGetService.getMemberBySocialId(socialId); + Team team=teamGetService.getTeamByTeamId(teamId); + Mission mission=missionQueryService.findMissionById(missionId); + createMissionReadUseCase.createMissionRead(team, member, mission); + return new MissionConfirmRes(mission.getId()); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionRemindAlarmUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionRemindAlarmUseCase.java new file mode 100644 index 00000000..2a43ef79 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionRemindAlarmUseCase.java @@ -0,0 +1,160 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.application.service.SaveMultiAlarmHistoryUseCase; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveScheduleQueryService; +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.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +import static com.moing.backend.global.config.fcm.constant.RemindMissionTitle.*; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionRemindAlarmUseCase { + + private final MissionArchiveScheduleQueryService missionArchiveScheduleQueryService; + private final MissionQueryService missionQueryService; + + private final MultiMessageSender multiMessageSender; + private final SaveMultiAlarmHistoryUseCase saveMultiAlarmHistoryUseCase; + + String REMIND_NAME = "미션 리마인드"; + + + public void sendRemindMissionAlarm() { + + Random random = new Random(System.currentTimeMillis()); + String title = getTitle(random.nextInt(4)); + String message = getMessage(random.nextInt(4)); + + List remainMissionPeople = missionArchiveScheduleQueryService.getRemainMissionPeople(); + + Optional> memberIdAndTokens = mapToMemberAndToken(remainMissionPeople); + Optional> pushMemberIdAndToken = isPushMemberIdAndToken(remainMissionPeople); + + if (pushMemberIdAndToken.isPresent() && !pushMemberIdAndToken.get().isEmpty()) { + multiMessageSender.send(new MultiRequest(pushMemberIdAndToken.get(), title, message, "", REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue())); + } + if (memberIdAndTokens.isPresent() && !memberIdAndTokens.get().isEmpty()) { + saveMultiAlarmHistoryUseCase.saveAlarmHistories(AlarmHistoryMapper.getMemberIds(memberIdAndTokens.get()),"",title,message,REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue()); + } + } + + + public String getTitle(int num) { + switch (num) { + case 0: + return REMIND_MISSION_TITLE1.getMessage(); + case 1: + return REMIND_MISSION_TITLE2.getMessage(); + case 2: + return REMIND_MISSION_TITLE3.getMessage(); + case 3: + return REMIND_MISSION_TITLE4.getMessage(); + } + return REMIND_MISSION_TITLE4.getMessage(); + + } + public String getMessage(int num) { + switch (num) { + case 0: + return REMIND_MISSION_MESSAGE1.getMessage(); + case 1: + return REMIND_MISSION_MESSAGE2.getMessage(); + case 2: + return REMIND_MISSION_MESSAGE3.getMessage(); + case 3: + return REMIND_MISSION_MESSAGE4.getMessage(); + } + return REMIND_MISSION_MESSAGE4.getMessage(); + + } + + + public Boolean sendRepeatMissionRemindOnSunday() { + + String title = REMIND_ON_SUNDAY_TITLE.getMessage(); + String message = REMIND_ON_SUNDAY_MESSAGE.getMessage(); + + List repeatMissionByStatus = missionQueryService.findRepeatMissionPeopleByStatus(MissionStatus.WAIT); + + Optional> memberIdAndTokens = mapToMemberAndToken(repeatMissionByStatus); + Optional> pushMemberIdAndToken = isPushMemberIdAndToken(repeatMissionByStatus); + + if (pushMemberIdAndToken.isPresent() && !pushMemberIdAndToken.get().isEmpty()) { + multiMessageSender.send(new MultiRequest(pushMemberIdAndToken.get(), title, message, "", REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue())); + } + if (memberIdAndTokens.isPresent() && !memberIdAndTokens.get().isEmpty()) { + saveMultiAlarmHistoryUseCase.saveAlarmHistories(AlarmHistoryMapper.getMemberIds(memberIdAndTokens.get()), "", title, message, REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue()); + } + return true; + + + } + + public Boolean sendRepeatMissionRemindOnMonday() { + + + String title = REMIND_ON_MONDAY_TITLE.getMessage(); + String message = REMIND_ON_MONDAY_MESSAGE.getMessage(); + + List repeatMissionByStatus = missionQueryService.findRepeatMissionPeopleByStatus(MissionStatus.ONGOING); + + Optional> memberIdAndTokens = mapToMemberAndToken(repeatMissionByStatus); + Optional> pushMemberIdAndToken = isPushMemberIdAndToken(repeatMissionByStatus); + + if (pushMemberIdAndToken.isPresent() && !pushMemberIdAndToken.get().isEmpty()) { + multiMessageSender.send(new MultiRequest(pushMemberIdAndToken.get(), title, message, "", REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue())); + } + if (memberIdAndTokens.isPresent() && !memberIdAndTokens.get().isEmpty()) { + saveMultiAlarmHistoryUseCase.saveAlarmHistories(AlarmHistoryMapper.getMemberIds(memberIdAndTokens.get()), "", title, message, REMIND_NAME, AlarmType.REMIND, PagePath.MISSION_ALL_PTAH.getValue()); + } + + return true; + + + } + + private Optional> mapToMemberAndToken(List members) { + return Optional.of(members.stream() + .map(member -> MemberIdAndToken.builder() + .fcmToken(member.getFcmToken()) + .memberId(member.getMemberId()) + .build()) + .collect(Collectors.toList())); + } + private Optional> isPushMemberIdAndToken(List members) { + return Optional.of(members.stream() + .map(member -> { + if (member.isRemindPush() && !member.isSignOut()) { + return MemberIdAndToken.builder() + .fcmToken(member.getFcmToken()) + .memberId(member.getMemberId()) + .build(); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + } + + + + +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionScheduleUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionScheduleUseCase.java new file mode 100644 index 00000000..05a19abb --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionScheduleUseCase.java @@ -0,0 +1,45 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.member.application.service.UpdateRemindAlarmUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Slf4j +@Service +@Transactional +@EnableAsync +@EnableScheduling +@RequiredArgsConstructor +@Profile("prod") +public class MissionScheduleUseCase { + + private final MissionRemindAlarmUseCase missionRemindAlarmUseCase; + private final MissionUpdateUseCase missionUpdateUseCase; + + /** + * 단일 미션 마감 + * 해당 시간 미션 마감 + * 한시간 마다 실행 + */ + @Scheduled(cron = "0 1 * * * *") + public void singleMissionEndRoutine() { + missionUpdateUseCase.terminateMissionByAdmin(); + } + + /** + * 리마인드 알림 + * 인증하지 않은 미션이 있는 경우 알림 + * 매일 오후 8시 + */ + @Scheduled(cron = "0 0 20 * * *") + public void MissionRemindAlarm() { + missionRemindAlarmUseCase.sendRemindMissionAlarm(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/MissionUpdateUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/MissionUpdateUseCase.java new file mode 100644 index 00000000..6e3f6bc6 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/MissionUpdateUseCase.java @@ -0,0 +1,72 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.application.dto.req.MissionReq; +import com.moing.backend.domain.mission.application.dto.res.MissionCreateRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.application.mapper.MissionMapper; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.mission.domain.service.MissionSaveService; +import com.moing.backend.domain.mission.exception.NoAccessUpdateMission; +import com.moing.backend.domain.team.domain.entity.Team; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionUpdateUseCase { + + private final MissionSaveService missionSaveService; + private final MissionQueryService missionQueryService; + private final MemberGetService memberGetService; + + public MissionCreateRes updateMission(String userSocialId, Long missionId, MissionReq missionReq) { + + Member member = memberGetService.getMemberBySocialId(userSocialId); + Mission mission = missionQueryService.findMissionById(missionId); + Team team = mission.getTeam(); + + Long memberId = member.getMemberId(); + + if (!((memberId.equals(mission.getMakerId())) || memberId.equals(team.getLeaderId())) ) { + throw new NoAccessUpdateMission(); + } + mission.updateMission(missionReq); + + return MissionMapper.mapToMissionCreateRes(mission,member); + + } + + public MissionReadRes terminateMissionByUser(String userSocialId, Long missionId) { + + Member member = memberGetService.getMemberBySocialId(userSocialId); + Long memberId = member.getMemberId(); + + Mission findMission = missionQueryService.findMissionById(missionId); + Team team = findMission.getTeam(); + + if ((memberId.equals(findMission.getMakerId())) || memberId.equals(team.getLeaderId()) ) { + findMission.updateStatus(MissionStatus.END); + findMission.updateDueTo(LocalDateTime.now()); + } else { + throw new NoAccessUpdateMission(); + } + + return MissionMapper.mapToMissionReadRes(findMission,member); + + } + + public void terminateMissionByAdmin() { + missionQueryService.findMissionByDueTo().stream().forEach( + mission -> mission.updateStatus(MissionStatus.END) + ); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/SendMissionCreateAlarmUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/SendMissionCreateAlarmUseCase.java new file mode 100644 index 00000000..029f5420 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/SendMissionCreateAlarmUseCase.java @@ -0,0 +1,59 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.history.domain.entity.AlarmType.NEW_UPLOAD; +import static com.moing.backend.global.config.fcm.constant.NewMissionTitle.NEW_SINGLE_MISSION_COMING; + +@Service +@Transactional +@RequiredArgsConstructor +public class SendMissionCreateAlarmUseCase { + + private final TeamMemberGetService teamMemberGetService; + private final ApplicationEventPublisher eventPublisher; + + public void sendNewMissionUploadAlarm(Member member, Mission mission) { + Team team = mission.getTeam(); + String title = team.getName() + " " + NEW_SINGLE_MISSION_COMING.getTitle(); + String message = mission.getTitle(); + String type = mission.getType().toString(); + String status = mission.getStatus().toString(); + + Optional> newUploadInfos=teamMemberGetService.getNewUploadInfo(team.getTeamId(), member.getMemberId()); + + Optional> memberIdAndTokensByPush = AlarmHistoryMapper.getNewUploadPushInfo(newUploadInfos); + Optional> memberIdAndTokensBySave = AlarmHistoryMapper.getNewUploadSaveInfo(newUploadInfos); + // 알림 보내기 + eventPublisher.publishEvent(new MultiFcmEvent(title, message, memberIdAndTokensByPush, memberIdAndTokensBySave, createIdInfo(team.getTeamId(), mission.getId(),mission.getType(),mission.getStatus()), team.getName(), NEW_UPLOAD, PagePath.MISSION_PATH.getValue())); + } + + private String createIdInfo(Long teamId, Long missionId,MissionType type, MissionStatus status) { + JSONObject jo = new JSONObject(); + jo.put("isRepeated", type.equals(MissionType.REPEAT)); + jo.put("teamId", teamId); + jo.put("missionId", missionId); + jo.put("status", status.name()); + jo.put("type", "NEW_UPLOAD_MISSION"); + return jo.toJSONString(); + } +} + diff --git a/src/main/java/com/moing/backend/domain/mission/application/service/SendMissionStartAlarmUseCase.java b/src/main/java/com/moing/backend/domain/mission/application/service/SendMissionStartAlarmUseCase.java new file mode 100644 index 00000000..be5ddebd --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/application/service/SendMissionStartAlarmUseCase.java @@ -0,0 +1,59 @@ +package com.moing.backend.domain.mission.application.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.global.config.fcm.constant.NewMissionTitle.NEW_REPEAT_MISSION_COMING; +import static com.moing.backend.global.config.fcm.constant.NewMissionTitle.NEW_SINGLE_MISSION_COMING; + +@Service +@Transactional +@RequiredArgsConstructor +public class SendMissionStartAlarmUseCase { + + private final TeamMemberGetService teamMemberGetService; + private final ApplicationEventPublisher eventPublisher; + + public void sendRepeatMissionStartAlarm(Mission mission) { + Team team = mission.getTeam(); + String title = team.getName() + " " + NEW_REPEAT_MISSION_COMING.getTitle(); + String message = mission.getTitle(); + String type = mission.getType().toString(); + String status = mission.getStatus().toString(); + + Optional> newUploadInfos=teamMemberGetService.getNewUploadInfo(team.getTeamId(), 0L); + + Optional> memberIdAndTokensByPush = AlarmHistoryMapper.getNewUploadPushInfo(newUploadInfos); + Optional> memberIdAndTokensBySave = AlarmHistoryMapper.getNewUploadSaveInfo(newUploadInfos); + // 알림 보내기 + eventPublisher.publishEvent(new MultiFcmEvent(title, message, memberIdAndTokensByPush, memberIdAndTokensBySave, createIdInfo(team.getTeamId(), mission.getId(),mission.getType(),mission.getStatus()), team.getName(), AlarmType.NEW_UPLOAD, PagePath.MISSION_PATH.getValue())); + } + + private String createIdInfo(Long teamId, Long missionId,MissionType type, MissionStatus status) { + JSONObject jo = new JSONObject(); + jo.put("isRepeated", type.equals(MissionType.REPEAT)); + jo.put("teamId", teamId); + jo.put("missionId", missionId); + jo.put("status", status.name()); + return jo.toJSONString(); + } +} + diff --git a/src/main/java/com/moing/backend/domain/mission/domain/entity/Mission.java b/src/main/java/com/moing/backend/domain/mission/domain/entity/Mission.java new file mode 100644 index 00000000..d5d6c80f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/entity/Mission.java @@ -0,0 +1,101 @@ +package com.moing.backend.domain.mission.domain.entity; + +import com.moing.backend.domain.mission.application.dto.req.MissionReq; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionRead.domain.entity.MissionRead; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class Mission extends BaseTimeEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "mission_id") + private Long id; + + private String title; + private LocalDateTime dueTo; + private String rule; + + @Column(nullable = false, columnDefinition="TEXT", length = 300) + private String content; + + private int number; + + private Long makerId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id") + private Team team; + + @Enumerated(value = EnumType.STRING) + private MissionType type; + + @Enumerated(value = EnumType.STRING) + private MissionStatus status; + + @Enumerated(value = EnumType.STRING) + private MissionWay way; + + @OneToMany(mappedBy = "mission") + List missionArchiveList = new ArrayList<>(); + + @OneToMany(mappedBy = "mission") + List missionReads=new ArrayList<>(); + + @Builder + public Mission(String title, LocalDateTime dueTo, String rule, String content, int number, Team team, MissionType type, MissionStatus status, MissionWay way,Long makerId) { + this.title = title; + this.dueTo = dueTo; + this.rule = rule; + this.content = content; + this.number = number; + this.team = team; + this.type = type; + this.status = status; + this.way = way; + this.makerId = makerId; + } + + + public void updateMission(MissionReq missionReq) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + this.title = missionReq.getTitle(); + this.dueTo = LocalDateTime.parse(missionReq.getDueTo(), formatter); + this.rule = missionReq.getRule(); + this.content = missionReq.getContent(); + this.number = missionReq.getNumber(); + this.type = MissionType.valueOf(missionReq.getType()); + this.way = MissionWay.valueOf(missionReq.getWay()); + + } + + public void setTeam(Team team) { + this.team = team; + } + + public void updateStatus(MissionStatus missionStatus) { + this.status = missionStatus; + } + + public void updateDueTo(LocalDateTime dueTo) { + this.dueTo = dueTo; + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionStatus.java b/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionStatus.java new file mode 100644 index 00000000..55f18654 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionStatus.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.mission.domain.entity.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MissionStatus { + END, + ONGOING, + SUCCESS, + FAIL, + WAIT +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionType.java b/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionType.java new file mode 100644 index 00000000..453a0a1f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionType.java @@ -0,0 +1,6 @@ +package com.moing.backend.domain.mission.domain.entity.constant; + +public enum MissionType { + + ONCE,REPEAT; +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionWay.java b/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionWay.java new file mode 100644 index 00000000..7fa9100b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/entity/constant/MissionWay.java @@ -0,0 +1,9 @@ +package com.moing.backend.domain.mission.domain.entity.constant; + +public enum MissionWay { + TEXT, + LINK, + PHOTO; + + private String name; +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionCustomRepository.java b/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionCustomRepository.java new file mode 100644 index 00000000..63dbd117 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionCustomRepository.java @@ -0,0 +1,41 @@ +package com.moing.backend.domain.mission.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.GatherRepeatMissionRes; +import com.moing.backend.domain.mission.application.dto.res.GatherSingleMissionRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface MissionCustomRepository { + Long findMissionsCountByTeam(Long teamId); + + Optional> findSingleMissionByMemberId(Long memberId, List teams); + + Optional> findRepeatMissionByMemberId(Long memberId, List teams); + + Optional> findMissionByDueTo(); + + Optional> findOngoingRepeatMissions(); + + Optional> findRepeatMissionByStatus(MissionStatus missionStatus); + + Optional> findRepeatMissionPeopleByStatus(MissionStatus missionStatus); + + boolean findRepeatMissionsByTeamId(Long teamId); + + Optional findByIds(Long memberId, Long missionId); + + Long getTodayOnceMissions(); + + Long getYesterdayOnceMissions(); + + Long getTodayRepeatMissions(); + + Long getYesterdayRepeatMissions(); +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionCustomRepositoryImpl.java new file mode 100644 index 00000000..ac2a84d5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionCustomRepositoryImpl.java @@ -0,0 +1,358 @@ +package com.moing.backend.domain.mission.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.GatherRepeatMissionRes; +import com.moing.backend.domain.mission.application.dto.res.GatherSingleMissionRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.QMission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.*; + +import static com.moing.backend.domain.mission.domain.entity.QMission.mission; +import static com.moing.backend.domain.missionArchive.domain.entity.QMissionArchive.missionArchive; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; +import static com.querydsl.jpa.JPAExpressions.select; + +public class MissionCustomRepositoryImpl implements MissionCustomRepository{ + + private final JPAQueryFactory queryFactory; + public MissionCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + + @Override + public Long findMissionsCountByTeam(Long teamId) { + return queryFactory + .select(mission.count()) + .from(mission) + .where( + mission.team.teamId.eq(teamId) + ) + .fetchFirst(); + } + + @Override + public Optional> findRepeatMissionByMemberId(Long memberId,Listteams) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.ofNullable(queryFactory + .select(Projections.constructor(GatherRepeatMissionRes.class, + mission.id, + mission.team.teamId, + mission.team.name, + mission.title, + mission.number.stringValue(), + missionArchive.count.max().coalesce(0L).stringValue(), + missionArchive.status.coalesce(mission.status).stringValue(), + + JPAExpressions + .select(teamMember.member.countDistinct().stringValue()) + .from(teamMember) + .where( + teamMember.team.eq(mission.team), + teamMember.member.memberId.in(RepeatMissionDonePeopleByWeek(mission.id)) + .or(teamMember.member.memberId.in(RepeatMissionDonePeopleByDay(mission.id))), + teamMember.isDeleted.ne(Boolean.TRUE) + ), + + mission.team.numOfMember.stringValue() + + )) + .from(mission) + .leftJoin(missionArchive) + .on(missionArchive.mission.eq(mission), + missionArchive.member.memberId.eq(memberId), + dateInRange + ) + .where( + mission.team.teamId.in(teams), + mission.status.eq(MissionStatus.ONGOING), + mission.type.eq(MissionType.REPEAT) + + ) + .groupBy(mission) + .orderBy(mission.createdDate.desc()) + .fetch()); + } + + private JPQLQuery RepeatMissionDonePeopleByDay(NumberPath missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + BooleanExpression hasAlreadyVerifiedToday = hasAlreadyVerifiedToday(); + + return + select(missionArchive.member.memberId) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + (missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange).and(hasAlreadyVerifiedToday)) + ).distinct(); + } + + private JPQLQuery RepeatMissionDonePeopleByWeek(NumberPath missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return select(missionArchive.member.memberId) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + (missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange)) + ) + .groupBy(missionArchive.mission.number, missionArchive.count) + .having( + missionArchive.count.max().goe(missionArchive.mission.number)) + .distinct(); + + } + + + @Override + public Optional> findMissionByDueTo() { + + return Optional.ofNullable(queryFactory + .selectFrom(mission) + .where( + mission.status.eq(MissionStatus.WAIT).or(mission.status.eq(MissionStatus.ONGOING)), + mission.dueTo.before(LocalDateTime.now()), + mission.type.eq(MissionType.ONCE) + ).fetch()); + } + + @Override + public Optional> findOngoingRepeatMissions() { + return Optional.ofNullable(queryFactory + .select(mission.id) + .from(mission) + .where(mission.status.eq(MissionStatus.ONGOING), + mission.type.eq(MissionType.REPEAT)) + .fetch()); + } + + @Override + public Optional> findRepeatMissionPeopleByStatus(MissionStatus missionStatus) { + + return Optional.ofNullable(queryFactory + .select(teamMember.member).distinct() + .from(teamMember) + .join(mission) + .on(teamMember.team.eq(mission.team), + teamMember.member.isDeleted.ne(true), + teamMember.team.isDeleted.ne(true)) + .where( + mission.status.eq(missionStatus), + mission.type.eq(MissionType.REPEAT) + ).fetch()); + + + } + @Override + public Optional> findRepeatMissionByStatus(MissionStatus missionStatus) { + return Optional.ofNullable(queryFactory + .select(mission) + .from(mission) + .where(mission.status.eq(missionStatus), + mission.type.eq(MissionType.REPEAT)) + .fetch()); + } + + + @Override + public Optional> findSingleMissionByMemberId(Long memberId, List teams) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.ofNullable(queryFactory + .select(Projections.constructor(GatherSingleMissionRes.class, + mission.id, + mission.team.teamId, + mission.team.name, + mission.title, + mission.dueTo.stringValue(), + missionArchive.status.coalesce(mission.status).stringValue(), + + JPAExpressions + .select(missionArchive.member.count().stringValue()) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(mission.id), + missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + ), + + mission.team.numOfMember.stringValue() + )) + .from(mission) + .leftJoin(missionArchive) + .on( + mission.eq(missionArchive.mission), + missionArchive.member.memberId.eq(memberId) + ) + .where( + mission.team.teamId.in(teams), + mission.status.eq(MissionStatus.ONGOING).or(mission.status.eq(MissionStatus.WAIT)), + mission.type.eq(MissionType.ONCE) + ) + .orderBy(missionArchive.status.asc(),mission.dueTo.asc(),missionArchive.createdDate.desc()) + .fetch()); + } + + + + public boolean findRepeatMissionsByTeamId(Long teamId) { + return queryFactory + .select(mission) + .from(mission) + .where( + mission.team.teamId.eq(teamId), + mission.type.eq(MissionType.REPEAT), + mission.status.eq(MissionStatus.ONGOING) + ).fetchCount() > 2; + } + + + + @Override + public Optional findByIds(Long memberId, Long missionId) { + + Mission mission = queryFactory + .selectFrom(QMission.mission) + .where(QMission.mission.id.eq(missionId)) + .fetchOne(); + + if (mission == null) { + return Optional.empty(); + } + + boolean isLeader = mission.getMakerId().equals(memberId) || mission.getTeam().getLeaderId().equals(memberId); + + MissionReadRes result = new MissionReadRes( + mission.getTitle(), + mission.getDueTo().toString(), + mission.getRule(), + mission.getContent(), + mission.getType().toString(), + mission.getWay().toString(), + isLeader + ); + + return Optional.of(result); + } + + @Override + public Long getTodayOnceMissions() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + + long todayOnceMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfToday, endOfToday) + .and(mission.type.eq(MissionType.ONCE))) + .fetchCount(); + + return todayOnceMissions; + + } + + @Override + public Long getYesterdayOnceMissions() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long yesterdayOnceMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfYesterday, startOfToday) + .and(mission.type.eq(MissionType.ONCE))) + .fetchCount(); + + return yesterdayOnceMissions; + } + + @Override + public Long getTodayRepeatMissions() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long todayRepeatMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfToday, endOfToday) + .and(mission.type.eq(MissionType.REPEAT))) + .fetchCount(); + + return todayRepeatMissions; + } + + @Override + public Long getYesterdayRepeatMissions() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long yesterdayRepeatMissions = queryFactory + .select(mission) + .from(mission) + .where(mission.createdDate.between(startOfYesterday, startOfToday) + .and(mission.type.eq(MissionType.REPEAT))) + .fetchCount(); + + + return yesterdayRepeatMissions; + } + + + + + private BooleanExpression createRepeatTypeConditionByArchive() { + LocalDate now = LocalDate.now(); + DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY; + LocalDate startOfWeek = now.with(TemporalAdjusters.previousOrSame(firstDayOfWeek)); + LocalDate endOfWeek = startOfWeek.plusDays(6); + + return missionArchive.createdDate.goe(startOfWeek.atStartOfDay()) + .and(missionArchive.createdDate.loe(endOfWeek.atStartOfDay().plusDays(1).minusNanos(1))); + + } + + private BooleanExpression hasAlreadyVerifiedToday() { + LocalDateTime today = LocalDateTime.now(); + LocalDateTime startOfToday = today.withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfToday = today.withHour(23).withMinute(59).withSecond(59).withNano(999999999); + + return missionArchive.createdDate.between(startOfToday, endOfToday); + } + + + +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionRepository.java b/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionRepository.java new file mode 100644 index 00000000..28dc00ec --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/repository/MissionRepository.java @@ -0,0 +1,9 @@ +package com.moing.backend.domain.mission.domain.repository; + +import com.moing.backend.domain.mission.domain.entity.Mission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MissionRepository extends JpaRepository,MissionCustomRepository { +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/service/MissionDeleteService.java b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionDeleteService.java new file mode 100644 index 00000000..8af04d37 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionDeleteService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.mission.domain.service; + +import com.moing.backend.domain.mission.domain.repository.MissionRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionDeleteService { + + private final MissionRepository missionRepository; + + public Long deleteMission(Long missionId) { + missionRepository.deleteById(missionId); + return missionId; + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/service/MissionQueryService.java b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionQueryService.java new file mode 100644 index 00000000..a1f9178f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionQueryService.java @@ -0,0 +1,102 @@ +package com.moing.backend.domain.mission.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.GatherRepeatMissionRes; +import com.moing.backend.domain.mission.application.dto.res.GatherSingleMissionRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.repository.MissionRepository; +import com.moing.backend.domain.mission.exception.NotFoundEndMissionException; +import com.moing.backend.domain.mission.exception.NotFoundMissionException; +import com.moing.backend.domain.mission.exception.NotFoundOngoingMissionException; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionQueryService { + + private final MissionRepository missionRepository; + private final TeamGetService teamGetService; + + public Mission findMissionById(Long missionId) { + return missionRepository.findById(missionId).orElseThrow(NotFoundMissionException::new); + } + public MissionReadRes findMissionByIds(Long memberId, Long missionId) { + return missionRepository.findByIds(memberId,missionId).orElseThrow(NotFoundMissionException::new); + } + + public Long findMissionsCountByTeam(Long teamId) { + return missionRepository.findMissionsCountByTeam(teamId); + } + + public List findAllRepeatMission(Long memberId) { + List teams = teamGetService.getTeamIdByMemberId(memberId); + return missionRepository.findRepeatMissionByMemberId(memberId,teams).orElseThrow(NotFoundMissionException::new); + } + + public List findAllSingleMission(Long memberId) { + List teams = teamGetService.getTeamIdByMemberId(memberId); + return missionRepository.findSingleMissionByMemberId(memberId, teams).orElseThrow(NotFoundMissionException::new); + } + + public List findTeamRepeatMission(Long memberId,Long teamId) { + List teams = new ArrayList<>(); + teams.add(teamId); + return missionRepository.findRepeatMissionByMemberId(memberId,teams).orElseThrow(NotFoundMissionException::new); + } + + public List findTeamSingleMission(Long memberId,Long teamId) { + List teams = new ArrayList<>(); + teams.add(teamId); + return missionRepository.findSingleMissionByMemberId(memberId, teams).orElseThrow(NotFoundMissionException::new); + } + + /** + * 스케쥴러에서 한시간 단위로 실행 + * 현재 시간으로부터 1시간 이내 종료 되는 미션 리턴 + */ + public List findMissionByDueTo() { + return missionRepository.findMissionByDueTo().orElseThrow(NotFoundEndMissionException::new); + } + + public List findOngoingRepeatMissions() { + return missionRepository.findOngoingRepeatMissions().orElseThrow(NotFoundOngoingMissionException::new); + } + + public boolean isAbleCreateRepeatMission(Long teamId) { + return missionRepository.findRepeatMissionsByTeamId(teamId); + } + + public List findRepeatMissionPeopleByStatus(MissionStatus missionStatus) { + return missionRepository.findRepeatMissionPeopleByStatus(missionStatus).orElseThrow(NotFoundMissionException::new + ); + } + public List findRepeatMissionByStatus(MissionStatus missionStatus) { + return missionRepository.findRepeatMissionByStatus(missionStatus).orElseThrow(NotFoundMissionException::new + ); + } + + public Long getTodayOnceMissions(){ + return missionRepository.getTodayOnceMissions(); + } + + public Long getYesterdayOnceMissions(){ + return missionRepository.getYesterdayOnceMissions(); + } + + public Long getTodayRepeatMissions(){ + return missionRepository.getTodayRepeatMissions(); + } + + public Long getYesterdayRepeatMissions(){ + return missionRepository.getYesterdayRepeatMissions(); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/service/MissionSaveService.java b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionSaveService.java new file mode 100644 index 00000000..e2155900 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionSaveService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.mission.domain.service; + +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.repository.MissionRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionSaveService { + + private final MissionRepository missionRepository; + + public void save(Mission mission) { + missionRepository.save(mission); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/domain/service/MissionUpdateService.java b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionUpdateService.java new file mode 100644 index 00000000..db3e7cff --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/domain/service/MissionUpdateService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.mission.domain.service; + +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.repository.MissionRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionUpdateService { + + private final MissionRepository missionRepository; + + public void updateMission(Mission mission) { + missionRepository.save(mission); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/MissionException.java b/src/main/java/com/moing/backend/domain/mission/exception/MissionException.java new file mode 100644 index 00000000..5b33e7c8 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/MissionException.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class MissionException extends ApplicationException { + + protected MissionException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } + +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NoAccessCreateMission.java b/src/main/java/com/moing/backend/domain/mission/exception/NoAccessCreateMission.java new file mode 100644 index 00000000..d8fa3346 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NoAccessCreateMission.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoAccessCreateMission extends MissionException { + + public NoAccessCreateMission() { + super(ErrorCode.NO_ACCESS_CREATE_MISSION, + HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NoAccessDeleteMission.java b/src/main/java/com/moing/backend/domain/mission/exception/NoAccessDeleteMission.java new file mode 100644 index 00000000..5265c4c7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NoAccessDeleteMission.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoAccessDeleteMission extends MissionException { + + public NoAccessDeleteMission() { + super(ErrorCode.NO_ACCESS_DELETE_MISSION, + HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NoAccessUpdateMission.java b/src/main/java/com/moing/backend/domain/mission/exception/NoAccessUpdateMission.java new file mode 100644 index 00000000..e1a5045b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NoAccessUpdateMission.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoAccessUpdateMission extends MissionException { + + public NoAccessUpdateMission() { + super(ErrorCode.NO_ACCESS_UPDATE_MISSION, + HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NoMoreCreateMission.java b/src/main/java/com/moing/backend/domain/mission/exception/NoMoreCreateMission.java new file mode 100644 index 00000000..76c525f4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NoMoreCreateMission.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoMoreCreateMission extends MissionException { + + public NoMoreCreateMission() { + super(ErrorCode.NO_MORE_CREATE_MISSION, + HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NotFoundEndMissionException.java b/src/main/java/com/moing/backend/domain/mission/exception/NotFoundEndMissionException.java new file mode 100644 index 00000000..942f2861 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NotFoundEndMissionException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundEndMissionException extends MissionException { + + public NotFoundEndMissionException() { + super(ErrorCode.NOT_FOUND_MISSION, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NotFoundMissionException.java b/src/main/java/com/moing/backend/domain/mission/exception/NotFoundMissionException.java new file mode 100644 index 00000000..7e67495f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NotFoundMissionException.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundMissionException extends MissionException { + + public NotFoundMissionException() { + super(ErrorCode.NOT_FOUND_MISSION, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/exception/NotFoundOngoingMissionException.java b/src/main/java/com/moing/backend/domain/mission/exception/NotFoundOngoingMissionException.java new file mode 100644 index 00000000..428458cf --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/exception/NotFoundOngoingMissionException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mission.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundOngoingMissionException extends MissionException { + + public NotFoundOngoingMissionException() { + super(ErrorCode.NOT_FOUND_MISSION, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/mission/presentation/MissionController.java b/src/main/java/com/moing/backend/domain/mission/presentation/MissionController.java new file mode 100644 index 00000000..56dccc52 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/presentation/MissionController.java @@ -0,0 +1,119 @@ +package com.moing.backend.domain.mission.presentation; + +import com.moing.backend.domain.mission.application.dto.req.MissionReq; +import com.moing.backend.domain.mission.application.dto.res.MissionCreateRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.application.dto.res.MissionConfirmRes; +import com.moing.backend.domain.mission.application.service.*; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import static com.moing.backend.domain.mission.presentation.constant.MissionResponseMessage.*; + + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team/{teamId}/missions") +public class MissionController { + + private final MissionCreateUseCase missionCreateUseCase; + private final MissionReadUseCase missionReadUseCase; + private final MissionUpdateUseCase missionUpdateUseCase; + private final MissionDeleteUseCase missionDeleteUseCase; + + /** + * 미션 조회 + * [GET] {teamId}/missions/{missionId} + * 작성자 : 정승연 + */ + + @GetMapping("/{missionId}") + public ResponseEntity> getMission(@AuthenticationPrincipal User user,@PathVariable("teamId") Long teamId, @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(READ_MISSION_SUCCESS.getMessage(), this.missionReadUseCase.getMission(user.getSocialId(),missionId))); + } + + /** + * 미션 생성 + * [POST] {teamId}/missions + * 작성자 : 정승연 + */ + + @PostMapping() + public ResponseEntity> createMission(@AuthenticationPrincipal User user,@PathVariable("teamId") Long teamId, @RequestBody MissionReq missionReq) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_MISSION_SUCCESS.getMessage(), this.missionCreateUseCase.createMission(user.getSocialId(),teamId,missionReq))); + } + + /** + * 미션 수정 + * [PUT] {teamId}/missions/{missionId} + * 작성자 : 정승연 + */ + @PutMapping("/{missionId}") + public ResponseEntity> updateMission(@AuthenticationPrincipal User user,@PathVariable("teamId") Long teamId, @PathVariable("missionId") Long missionId, @RequestBody MissionReq missionReq) { + return ResponseEntity.ok(SuccessResponse.create(UPDATE_MISSION_SUCCESS.getMessage(), this.missionUpdateUseCase.updateMission(user.getSocialId(),missionId, missionReq))); + } + + /** + * 미션 종료 + * [PUT] {teamId}/missions/{missionId}/end + * 작성자 : 정승연 + */ + @PutMapping("/{missionId}/end") + public ResponseEntity> terminateMission(@AuthenticationPrincipal User user, @PathVariable("teamId") Long teamId, @PathVariable Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(END_MISSION_SUCCESS.getMessage(), this.missionUpdateUseCase.terminateMissionByUser(user.getSocialId(),missionId))); + } + + /** + * 미션 삭제 + * [DELETE] {teamId}/missions/{missionId} + * 작성자 : 정승연 + */ + @DeleteMapping("/{missionId}") + public ResponseEntity> deleteMission(@AuthenticationPrincipal User user,@PathVariable("teamId") Long teamId,@PathVariable Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(DELETE_MISSION_SUCCESS.getMessage(), this.missionDeleteUseCase.deleteMission(user.getSocialId(),missionId))); + } + + /** + * 미션 추천 + * [GET] {teamId}/missions/recommend + * 작성자 : 정승연 + */ + + @GetMapping("/recommend") + public ResponseEntity> recommendMission(@AuthenticationPrincipal User user,@PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(RECOMMEND_MISSION_SUCCESS.getMessage(), this.missionReadUseCase.getTeamCategory(teamId))); + } + + /** + * 미션 추천 + * [GET] {teamId}/missions/isLeader + * 작성자 : 정승연 + */ + + @GetMapping("/isLeader") + public ResponseEntity> isLeader(@AuthenticationPrincipal User user,@PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(RECOMMEND_MISSION_SUCCESS.getMessage(), this.missionCreateUseCase.getIsLeader(user.getSocialId(),teamId))); + } + + + /** + * 미션 설명 확인 (미션 읽음 처리) + * [POST] {teamId}/missions/{missionId}/read + * 작성자 : 김민수 + */ + @PostMapping("/{missionId}/confirm") + public ResponseEntity> confirmMissionExplanation(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long missionId){ + return ResponseEntity.ok(SuccessResponse.create(CONFIRM_MISSION_SUCCESS.getMessage(), this.missionReadUseCase.confirmMission(user.getSocialId(), missionId, teamId))); + + } + + + + +} diff --git a/src/main/java/com/moing/backend/domain/mission/presentation/constant/MissionResponseMessage.java b/src/main/java/com/moing/backend/domain/mission/presentation/constant/MissionResponseMessage.java new file mode 100644 index 00000000..14eec075 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mission/presentation/constant/MissionResponseMessage.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.mission.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MissionResponseMessage { + CREATE_MISSION_SUCCESS("미션 생성을 완료 했습니다"), + READ_MISSION_SUCCESS("미션 조회를 완료 했습니다"), + UPDATE_MISSION_SUCCESS("미션 수정을 완료 했습니다"), + END_MISSION_SUCCESS("미션 종료를 완료 했습니다"), + DELETE_MISSION_SUCCESS("미션 삭제를 완료 했습니다"), + RECOMMEND_MISSION_SUCCESS("미션 추천을 위한 팀 카테고리를 조회 했습니다"), + CONFIRM_MISSION_SUCCESS("미션 설명을 확인했습니다."); + + private final String message; +} + diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/req/MissionArchiveHeartReq.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/req/MissionArchiveHeartReq.java new file mode 100644 index 00000000..db569206 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/req/MissionArchiveHeartReq.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.missionArchive.application.dto.req; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MissionArchiveHeartReq { + private Long archiveId; + private String heartStatus; +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/req/MissionArchiveReq.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/req/MissionArchiveReq.java new file mode 100644 index 00000000..bba83666 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/req/MissionArchiveReq.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.missionArchive.application.dto.req; + +import lombok.*; + +import javax.annotation.Nullable; +import javax.validation.constraints.Size; + +@Builder +@NoArgsConstructor +@Getter +public class MissionArchiveReq { + + private String status; + + @Size(min = 1, max = 1000) + private String archive; //사진일 경우 파일명, 이외에는 text,link + + private String contents; + + @Builder + public MissionArchiveReq(String status, String archive, String contents) { + this.status = status; + this.archive = archive; + this.contents = contents; + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveHeartRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveHeartRes.java new file mode 100644 index 00000000..97314466 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveHeartRes.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MissionArchiveHeartRes { + private Long archiveId; + private String heartStatus; + private int hearts; +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchivePhotoRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchivePhotoRes.java new file mode 100644 index 00000000..331ae3e1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchivePhotoRes.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MissionArchivePhotoRes { + Long teamId; + List photo; +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveRes.java new file mode 100644 index 00000000..871786c3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveRes.java @@ -0,0 +1,29 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class MissionArchiveRes { + + private Long archiveId; + private String archive; + private String way; + private String createdDate; + private String status; + private Long count; + private String heartStatus; + private Long hearts; + private String contents; + private Long comments; + + public void updateCount(Long count) { + this.count = count; + } + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveStatusRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveStatusRes.java new file mode 100644 index 00000000..89c30696 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MissionArchiveStatusRes.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class MissionArchiveStatusRes { + private String total; + private String done; +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyArchiveStatus.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyArchiveStatus.java new file mode 100644 index 00000000..102b47ea --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyArchiveStatus.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.*; + +@ToString +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class MyArchiveStatus { + private boolean end; + private String status; +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyMissionArchiveRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyMissionArchiveRes.java new file mode 100644 index 00000000..3d15bd08 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyMissionArchiveRes.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class MyMissionArchiveRes { + private String today; + private List archives; + + public void updateArchives(List archives) { + this.archives = archives; + } + + public void updateTodayStatus(Boolean bo) { + if (bo) { + this.today = "True"; + } else { + this.today = "False"; + } + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyTeamsRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyTeamsRes.java new file mode 100644 index 00000000..e665c563 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/MyTeamsRes.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Builder +public class MyTeamsRes { + private Long teamId; + private String teamName; +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/PersonalArchiveRes.java b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/PersonalArchiveRes.java new file mode 100644 index 00000000..5ed6a0bb --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/dto/res/PersonalArchiveRes.java @@ -0,0 +1,33 @@ +package com.moing.backend.domain.missionArchive.application.dto.res; + +import lombok.*; + +import javax.annotation.Nullable; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class PersonalArchiveRes { + + private Long archiveId; + private String nickname; + private String profileImg; + + private String archive; + private String createdDate; + private String way; + + private String heartStatus; + private int hearts; + + private String status; + private Long count; + + private Long makerId; + + @Nullable + private String contents; + private Long comments; + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/mapper/MissionArchiveMapper.java b/src/main/java/com/moing/backend/domain/missionArchive/application/mapper/MissionArchiveMapper.java new file mode 100644 index 00000000..6c4b40ae --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/mapper/MissionArchiveMapper.java @@ -0,0 +1,156 @@ +package com.moing.backend.domain.missionArchive.application.mapper; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveHeartRes; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveRes; +import com.moing.backend.domain.missionArchive.application.dto.res.PersonalArchiveRes; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchiveStatus; +import com.moing.backend.domain.missionHeart.domain.constant.MissionHeartStatus; +import com.moing.backend.global.annotation.Mapper; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +@Transactional +@Mapper +public class MissionArchiveMapper { + + public static MissionArchive mapToMissionArchive(MissionArchiveReq missionArchiveReq, Member member, Mission mission) { + return MissionArchive.builder() + .archive(missionArchiveReq.getArchive()) + .status(MissionArchiveStatus.valueOf(missionArchiveReq.getStatus())) + .member(member) + .mission(mission) + .heartList(new ArrayList<>()) + .contents(missionArchiveReq.getContents()) + .commentNum(0L) + .build(); + } + + public static MissionArchiveRes mapToMissionArchiveRes(MissionArchive missionArchive,Long memberId) { + return MissionArchiveRes.builder() + .archiveId(missionArchive.getId()) + .archive(missionArchive.getArchive()) + .way(missionArchive.getMission().getWay().toString()) + .createdDate(missionArchive.getCreatedDate().toString()) + .status(missionArchive.getStatus().name()) + .count(missionArchive.getCount()) + .heartStatus( + String.valueOf( + missionArchive.getHeartList().stream().anyMatch( + missionHeart -> missionHeart.getPushMemberId().equals(memberId) + && missionHeart.getHeartStatus() == (MissionHeartStatus.True) + ))) + .hearts(missionArchive.getHeartList().stream() + .filter(heart -> heart.getHeartStatus() == ( MissionHeartStatus.True)) + .filter(heart -> heart.getMissionArchive().equals( missionArchive))// heartStatus가 true인 요소만 필터링 + .count()) + .contents(missionArchive.getContents()) + .comments(missionArchive.getCommentNum()) + .build(); + } + + public static List mapToMissionArchiveResList(List missionArchiveList,Long memberId) { + List missionArchiveResList = new ArrayList<>(); + missionArchiveList.forEach( + missionArchive -> missionArchiveResList.add(MissionArchiveMapper.mapToMissionArchiveRes(missionArchive,memberId)) + ); + return missionArchiveResList; + } + + + public static PersonalArchiveRes mapToPersonalArchive(MissionArchive missionArchive,Long memberId) { + Member member = missionArchive.getMember(); + return PersonalArchiveRes.builder() + .archiveId(missionArchive.getId()) + .nickname(member.getNickName()) + .profileImg(member.getProfileImage()) + .archive(missionArchive.getArchive()) + .way(missionArchive.getMission().getWay().toString()) + .createdDate(missionArchive.getCreatedDate().toString()) + .status(missionArchive.getStatus().name()) + .count(missionArchive.getCount()) + .heartStatus( + String.valueOf( + missionArchive.getHeartList().stream().anyMatch( + missionHeart -> missionHeart.getPushMemberId().equals(memberId) + && missionHeart.getHeartStatus() == (MissionHeartStatus.True) + ))) + .hearts((int) missionArchive.getHeartList().stream() + .filter(heart -> heart.getHeartStatus().equals( MissionHeartStatus.True)) + .filter(heart -> heart.getMissionArchive().equals( missionArchive))// heartStatus가 true인 요소만 필터링 + .count()) + .makerId(missionArchive.getMember().getMemberId()) + .contents(missionArchive.getContents()) + .comments(missionArchive.getCommentNum()) + .build(); + } + + public static List mapToPersonalArchiveList(List missionArchiveList,Long memberId) { + List personalArchiveList = new ArrayList<>(); + missionArchiveList.forEach( + missionArchive -> personalArchiveList.add(MissionArchiveMapper.mapToPersonalArchive(missionArchive,memberId)) + ); + return personalArchiveList; + } + + + public static SingleMissionBoardRes mapToSingleMissionBoardRes(MissionArchive missionArchive) { + Member member = missionArchive.getMember(); + Mission mission = missionArchive.getMission(); + return SingleMissionBoardRes.builder() + .missionId(mission.getId()) + .title(mission.getTitle()) + .missionType(mission.getType().name()) + .dueTo(mission.getDueTo().toString()) + .status(missionArchive.getStatus().name()) + .build(); + + } + + public static List mapToSingleMissionBoardResList( List missionArchives) { + List singleMissionBoardResList = new ArrayList<>(); + missionArchives.forEach( + missionArchive -> singleMissionBoardResList.add(MissionArchiveMapper.mapToSingleMissionBoardRes(missionArchive)) + ); + + return singleMissionBoardResList; + } + + public static MissionArchiveHeartRes mapToMissionArchiveHeartRes(MissionArchive missionArchive,Boolean heartStatus) { + return MissionArchiveHeartRes.builder() + .archiveId(missionArchive.getId()) + .heartStatus(heartStatus.toString()) + .build(); + } + + + public static List mapToFinishMissionBoardResList(List missionArchives) { + List finishMissionBoardResList = new ArrayList<>(); + missionArchives.forEach( + missionArchive -> finishMissionBoardResList.add(MissionArchiveMapper.mapToFinishMissionBoardRes(missionArchive)) + ); + + return finishMissionBoardResList; + } + + public static FinishMissionBoardRes mapToFinishMissionBoardRes(MissionArchive missionArchive) { + Mission mission = missionArchive.getMission(); + return FinishMissionBoardRes.builder() + .missionId(mission.getId()) + .missionType(mission.getType().name()) + .title(mission.getTitle()) + .dueTo(mission.getDueTo().toString()) + .status(missionArchive.getStatus().name()) + .build(); + } + + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveBoardUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveBoardUseCase.java new file mode 100644 index 00000000..2a4d71ef --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveBoardUseCase.java @@ -0,0 +1,86 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.GatherSingleMissionRes; +import com.moing.backend.domain.mission.application.dto.res.RepeatMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveRes; +import com.moing.backend.domain.missionArchive.application.mapper.MissionArchiveMapper; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.repository.TeamRepository; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +// 모아보기 +@Service +@Transactional +@RequiredArgsConstructor +public class MissionArchiveBoardUseCase { + + private final MissionArchiveQueryService missionArchiveQueryService; + private final MissionQueryService missionQueryService; + + private final MemberGetService memberGetService; + private final TeamRepository teamRepository; + + /* + * 팀별 미션 보드 중 진행중인 단일 미션(한 번 인증) + */ + public List getActiveSingleMissions(Long teamId, String memberId) { + + Member member = memberGetService.getMemberBySocialId(memberId); + + return missionArchiveQueryService.findMySingleMissionArchives(member.getMemberId(), teamId, MissionStatus.ONGOING); + + } + + /* + * 팀별 미션 보드 중 종료한 전체 미션 + */ + public List getFinishMissions(Long teamId, String memberId) { + + Team team = teamRepository.findById(teamId).orElseThrow(); + Member member = memberGetService.getMemberBySocialId(memberId); + + return missionArchiveQueryService.findMyFinishMissions(member.getMemberId(), teamId); + + } + + public List getActiveRepeatMissions(Long teamId, String memberId) { + + Member member = memberGetService.getMemberBySocialId(memberId); + + List myRepeatMissionArchives = missionArchiveQueryService.findMyRepeatMissionArchives(member.getMemberId(), teamId, MissionStatus.ONGOING); + + + LocalDate currentDate = LocalDate.now(); + DayOfWeek currentDayOfWeek = currentDate.getDayOfWeek(); + if (currentDayOfWeek == DayOfWeek.SUNDAY) { + myRepeatMissionArchives.stream().forEach( + repeatMissionBoardRes -> repeatMissionBoardRes.setDueTo("True") + ); + } + + return myRepeatMissionArchives; + + } + + + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveCreateMessage.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveCreateMessage.java new file mode 100644 index 00000000..bb3564b9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveCreateMessage.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MissionArchiveCreateMessage { + + CREATOR_CREATE_MISSION_ARCHIVE("%s님이 미션을 인증했어요!"), + TEAM_AND_TITLE("[%s] %s"); + + private final String message; + + public String to(String creator) { + return String.format(message, creator); + } + + public String teamAndTitle(String team, String title) { + return String.format(message, team, title); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveCreateUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveCreateUseCase.java new file mode 100644 index 00000000..2979b706 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveCreateUseCase.java @@ -0,0 +1,134 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveRes; +import com.moing.backend.domain.missionArchive.application.mapper.MissionArchiveMapper; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveSaveService; +import com.moing.backend.domain.missionArchive.exception.NoMoreMissionArchiveException; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamScore.application.service.TeamScoreUpdateUseCase; +import com.moing.backend.domain.teamScore.domain.entity.ScoreStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class MissionArchiveCreateUseCase { + + private final MissionArchiveSaveService missionArchiveSaveService; + private final MissionArchiveQueryService missionArchiveQueryService; + + private final MissionQueryService missionQueryService; + private final MemberGetService memberGetService; + + private final TeamScoreUpdateUseCase teamScoreUpdateUseCase; + + private final SendMissionArchiveCreateAlarmUseCase sendMissionArchiveCreateAlarmUseCase; + + public MissionArchiveRes createArchive(String userSocialId, Long missionId, MissionArchiveReq missionReq) { + + Member member = memberGetService.getMemberBySocialId(userSocialId); + Long memberId = member.getMemberId(); + + Mission mission = missionQueryService.findMissionById(missionId); + MissionArchive newArchive = MissionArchiveMapper.mapToMissionArchive(missionReq, member, mission); + + // 인증 완료한 미션인지 확인 + if (isDoneMission(memberId,mission)) { + throw new NoMoreMissionArchiveException(); + } + + MissionArchiveRes missionArchiveRes; + + // 반복 미션일 경우 + if (mission.getType() == MissionType.REPEAT) { + + // 당일 1회 인증만 가능 + if (missionArchiveQueryService.isAbleToArchiveToday(memberId, missionId)) { + throw new NoMoreMissionArchiveException(); + } + + newArchive.updateCount(missionArchiveQueryService.findMyDoneArchives(memberId, missionId) + 1); + missionArchiveRes = MissionArchiveMapper.mapToMissionArchiveRes(missionArchiveSaveService.save(newArchive), memberId); + } + + // 한번 미션일 경우 + else { + + // 미션 생성 후 처음 미션 인증 시도 시 ongoing 으로 변경 -> 읽음처리 구현되면 로직 삭제 + if(mission.getStatus() == MissionStatus.WAIT) { + mission.updateStatus(MissionStatus.ONGOING); + } + + newArchive.updateCount(missionArchiveQueryService.findMyDoneArchives(memberId, missionId)+1); + missionArchiveRes = MissionArchiveMapper.mapToMissionArchiveRes(missionArchiveSaveService.save(newArchive), memberId); + + // 인증 후 n/n명 인증 성공 리턴값 업데이트 + Long doneSingleArchives = missionArchiveQueryService.findDoneSingleArchives(missionId); + missionArchiveRes.updateCount(doneSingleArchives); + + } + // 소모임원 3명 이상일 경우 보너스 점수 + if (mission.getTeam().getNumOfMember() > 2) { + gainBonusScore(mission, newArchive); + } + // 미션 인증 1회당 점수 + teamScoreUpdateUseCase.gainScoreOfArchive(mission, ScoreStatus.PLUS); + + // 미션 인증 시 다른 팀원에게 알림 전송 + sendMissionArchiveCreateAlarmUseCase.sendNewMissionArchiveUploadAlarm(member,mission); + + return missionArchiveRes; + } + + // 이 미션을 완료 했는지 + private Boolean isDoneMission(Long memberId,Mission mission) { + return missionArchiveQueryService.findMyDoneArchives(memberId, mission.getId()) >= mission.getNumber(); + } + + private void gainBonusScore(Mission mission, MissionArchive missionArchive) { + + if (mission.getType() == MissionType.ONCE) { + + if (isAbleToFinishOnceMission(mission)) { + mission.updateStatus(MissionStatus.SUCCESS); + teamScoreUpdateUseCase.gainScoreOfBonus(mission); + } + + } else { + if (isAbleToFinishRepeatMission(mission, missionArchive)) { + teamScoreUpdateUseCase.gainScoreOfBonus(mission); + + } + + } + } + + private boolean isAbleToFinishRepeatMission(Mission mission, MissionArchive archive) { + return mission.getNumber() <= archive.getCount(); + } + + private boolean isAbleToFinishOnceMission(Mission mission) { + + Team team = mission.getTeam(); + + Integer total = team.getNumOfMember(); + Long done = missionArchiveQueryService.stateCountByMissionId(mission.getId()); + + return done >= total; + + } + + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveDeleteUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveDeleteUseCase.java new file mode 100644 index 00000000..65e92852 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveDeleteUseCase.java @@ -0,0 +1,73 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchiveStatus; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveDeleteService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveSaveService; +import com.moing.backend.domain.missionArchive.exception.NoAccessMissionArchiveException; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentDeleteService; +import com.moing.backend.domain.missionHeart.domain.service.MissionHeartQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamScore.application.service.TeamScoreUpdateUseCase; +import com.moing.backend.domain.teamScore.domain.entity.ScoreStatus; +import com.moing.backend.global.utils.UpdateUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionArchiveDeleteUseCase { + + private final MissionArchiveQueryService missionArchiveQueryService; + private final MissionArchiveDeleteService missionArchiveDeleteService; + private final MissionQueryService missionQueryService; + private final MissionCommentDeleteService missionCommentDeleteService; + + private final MemberGetService memberGetService; + private final TeamScoreUpdateUseCase teamScoreUpdateUseCase; + + private final UpdateUtils updateUtils; + + + + public Long deleteArchive(String userSocialId, Long missionId,Long count) { + + Member member = memberGetService.getMemberBySocialId(userSocialId); + Long memberId = member.getMemberId(); + + Mission mission = missionQueryService.findMissionById(missionId); + + MissionArchive deleteArchive = missionArchiveQueryService.findOneMyArchive(memberId, missionId,count); + + LocalDateTime createdDate = deleteArchive.getCreatedDate(); + LocalDateTime today = LocalDateTime.now(); + + // 반복미션이면서 오늘 이전에 한 인증은 인증 취소할 수 없도록 + if (mission.getType().equals(MissionType.REPEAT) && createdDate.toLocalDate().isBefore(today.toLocalDate())) { + throw new NoAccessMissionArchiveException(); + } + + if (deleteArchive.getStatus().equals(MissionArchiveStatus.COMPLETE) && mission.getWay().equals(MissionWay.PHOTO)) { + String archive = deleteArchive.getArchive(); + updateUtils.deleteImgUrl(archive); + } + + missionCommentDeleteService.deleteAllCommentByMissionArchive(deleteArchive.getId()); + missionArchiveDeleteService.deleteMissionArchive(deleteArchive); + teamScoreUpdateUseCase.gainScoreOfArchive(mission, ScoreStatus.MINUS); + + return deleteArchive.getId(); + + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveReadUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveReadUseCase.java new file mode 100644 index 00000000..0ac0ccf0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveReadUseCase.java @@ -0,0 +1,87 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.application.dto.res.*; +import com.moing.backend.domain.missionArchive.application.mapper.MissionArchiveMapper; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionArchiveReadUseCase { + + //미션 아치브 읽어오기 + private final MemberGetService memberGetService; + private final MissionQueryService missionQueryService; + private final MissionArchiveQueryService missionArchiveQueryService; + private final TeamGetService teamGetService; + + + // 미션 인증 조회 + public MyMissionArchiveRes getMyArchive(String userSocialId, Long missionId) { + + Long memberId = memberGetService.getMemberBySocialId(userSocialId).getMemberId(); + + MyMissionArchiveRes myMissionArchiveRes = new MyMissionArchiveRes(); + + List missionArchiveRes = MissionArchiveMapper.mapToMissionArchiveResList(missionArchiveQueryService.findMyArchive(memberId, missionId), memberId); + myMissionArchiveRes.updateArchives(missionArchiveRes); + + myMissionArchiveRes.updateTodayStatus(missionArchiveQueryService.isAbleToArchiveToday(memberId, missionId)); + + return myMissionArchiveRes; + + } + + // 모두의 미션 인증 목록 조회 + public List getPersonalArchive(String userSocialId, Long missionId) { + + List personalArchives = new ArrayList<>(); + + Long memberId = memberGetService.getMemberBySocialId(userSocialId).getMemberId(); + return MissionArchiveMapper.mapToPersonalArchiveList(missionArchiveQueryService.findOthersArchive(memberId, missionId), memberId); + } + + public MissionArchiveStatusRes getMissionDoneStatus(Long missionId) { + Mission mission = missionQueryService.findMissionById(missionId); + Team team = mission.getTeam(); + + String done = "0"; + + if (mission.getType().equals(MissionType.ONCE)) { + done = missionArchiveQueryService.findDoneSingleArchives(missionId).toString(); + } else { + done = missionArchiveQueryService.findDoneRepeatArchives(missionId).toString(); + } + + return MissionArchiveStatusRes.builder() + .total(team.getNumOfMember().toString()) + .done(done) + .build(); + + } + + + public MyArchiveStatus getMissionArchiveStatus(String userSocialId,Long missionId , Long teamId) { + + Long memberId = memberGetService.getMemberBySocialId(userSocialId).getMemberId(); + + return missionArchiveQueryService.findMissionStatusById(memberId, missionId, teamId); + + } + + + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveUpdateUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveUpdateUseCase.java new file mode 100644 index 00000000..2d7222e7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/MissionArchiveUpdateUseCase.java @@ -0,0 +1,73 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveRes; +import com.moing.backend.domain.missionArchive.application.mapper.MissionArchiveMapper; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveSaveService; +import com.moing.backend.domain.missionArchive.exception.NoAccessMissionArchiveException; +import com.moing.backend.domain.team.domain.entity.Team; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionArchiveUpdateUseCase { + + private final MissionArchiveSaveService missionArchiveSaveService; + private final MissionArchiveQueryService missionArchiveQueryService; + + private final MissionQueryService missionQueryService; + + private final MemberGetService memberGetService; + + + // 미션 재인증 (수정하기도 포함됨) -> 사용하지 않음 + public MissionArchiveRes updateArchive(String userSocialId, Long missionId, MissionArchiveReq missionReq) { + + Member member = memberGetService.getMemberBySocialId(userSocialId); + Long memberId = member.getMemberId(); + Mission mission = missionQueryService.findMissionById(missionId); + Team team = mission.getTeam(); + + // 사진 제출 했다면, + if (mission.getWay() == MissionWay.PHOTO && missionArchiveQueryService.isDone(memberId, missionId)) { + //s3삭제 + + } + + MissionArchive updateArchive = missionArchiveQueryService.findMyArchive(memberId, missionId).get(0); + + // 단일 미션 && 미션 종료 직전인지 확인 +// if (mission.getType() == MissionType.ONCE && missionStateUseCase.isAbleToEnd(mission)) { +// mission.updateStatus(MissionStatus.SUCCESS); +// // 점수 반영 로직 +// +// } + // 반복미션의 경우 당일이 지나면 업데이트 불가능 + if (!(updateArchive.getLastModifiedDate().getDayOfWeek().equals(LocalDateTime.now().getDayOfWeek()))) { + throw new NoAccessMissionArchiveException(); + } + + updateArchive.updateArchive(missionReq); +// missionStateUseCase.updateMissionState(member, mission, updateArchive); + return MissionArchiveMapper.mapToMissionArchiveRes(missionArchiveSaveService.save(updateArchive),memberId); + + } + // 이 미션을 완료 했는지 + public Boolean isDoneMission(Long memberId,Mission mission) { + return missionArchiveQueryService.findMyDoneArchives(memberId, mission.getId()) >= mission.getNumber(); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/RepeatMissionArchiveReadUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/RepeatMissionArchiveReadUseCase.java new file mode 100644 index 00000000..02a17034 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/RepeatMissionArchiveReadUseCase.java @@ -0,0 +1,38 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveStatusRes; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class RepeatMissionArchiveReadUseCase { + + //미션 아치브 읽어오기 + private final MemberGetService memberGetService; + private final MissionQueryService missionQueryService; + private final MissionArchiveQueryService missionArchiveQueryService; + + public MissionArchiveStatusRes getMyMissionDoneStatus(String userSocialId,Long missionId) { + Member member = memberGetService.getMemberBySocialId(userSocialId); + Mission mission = missionQueryService.findMissionById(missionId); + Team team = mission.getTeam(); + + return MissionArchiveStatusRes.builder() + .total(String.valueOf(mission.getNumber())) + .done(missionArchiveQueryService.findMyDoneArchives(member.getMemberId(),missionId).toString()) + .build(); + + } + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/application/service/SendMissionArchiveCreateAlarmUseCase.java b/src/main/java/com/moing/backend/domain/missionArchive/application/service/SendMissionArchiveCreateAlarmUseCase.java new file mode 100644 index 00000000..fdba87a2 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/application/service/SendMissionArchiveCreateAlarmUseCase.java @@ -0,0 +1,60 @@ +package com.moing.backend.domain.missionArchive.application.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.missionArchive.application.service.MissionArchiveCreateMessage.CREATOR_CREATE_MISSION_ARCHIVE; +import static com.moing.backend.domain.missionArchive.application.service.MissionArchiveCreateMessage.TEAM_AND_TITLE; + +@Service +@Transactional +@RequiredArgsConstructor +public class SendMissionArchiveCreateAlarmUseCase { + + private final TeamMemberGetService teamMemberGetService; + private final ApplicationEventPublisher eventPublisher; + + public void sendNewMissionArchiveUploadAlarm(Member member, Mission mission) { + Team team = mission.getTeam(); + + String title = CREATOR_CREATE_MISSION_ARCHIVE.to(member.getNickName()); + String message = TEAM_AND_TITLE.teamAndTitle(team.getName(),mission.getTitle()); + + + Optional> newUploadInfos=teamMemberGetService.getNewUploadInfo(team.getTeamId(), member.getMemberId()); + + Optional> memberIdAndTokensByPush = AlarmHistoryMapper.getNewUploadPushInfo(newUploadInfos); + Optional> memberIdAndTokensBySave = AlarmHistoryMapper.getNewUploadSaveInfo(newUploadInfos); + // 알림 보내기 + eventPublisher.publishEvent(new MultiFcmEvent(title, message, memberIdAndTokensByPush, memberIdAndTokensBySave, createIdInfo(team.getTeamId(), mission.getId(),mission.getType(),mission.getStatus()), team.getName(), AlarmType.NEW_UPLOAD, PagePath.MISSION_PATH.getValue())); + } + + private String createIdInfo(Long teamId, Long missionId,MissionType type, MissionStatus status) { + JSONObject jo = new JSONObject(); + jo.put("isRepeated", type.equals(MissionType.REPEAT)); + jo.put("teamId", teamId); + jo.put("missionId", missionId); + jo.put("status", status.name()); + jo.put("type", "COMPLETE_MISSION"); + return jo.toJSONString(); + } +} + diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/constant/MissionArchiveResponseMessage.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/constant/MissionArchiveResponseMessage.java new file mode 100644 index 00000000..88ab1b4d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/constant/MissionArchiveResponseMessage.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.missionArchive.domain.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MissionArchiveResponseMessage { + + CREATE_ARCHIVE_SUCCESS("미션 인증을 완료 했습니다."), + UPDATE_ARCHIVE_SUCCESS("미션 재인증을 완료 했습니다."), + DELETE_ARCHIVE_SUCCESS("미션 인증 삭제를 완료 했습니다."), + READ_MY_ARCHIVE_SUCCESS("나의 미션 인증 현황 조회를 완료 했습니다."), + READ_TEAM_ARCHIVE_SUCCESS("팀원 미션 인증 현황 조회를 완료 했습니다."), + HEART_UPDATE_SUCCESS("미션 인증 좋아요를 성공적으로 눌렀습니다."), + MISSION_ARCHIVE_PEOPLE_STATUS_SUCCESS("미션 인증 성공한 인원 상태 조회를 완료 했습니다."), + MISSION_ARCHIVE_MY_STATUS_SUCCESS("미션 인증 성공한 나의 현황 조회를 완료 했습니다."), + ACTIVE_SINGLE_MISSION_SUCCESS("진행 중인 모든 한번 인증 미션 조회를 완료 했습니다."), + ACTIVE_REPEAT_MISSION_SUCCESS("진행 중인 모든 반복 인증 미션 조회를 완료 했습니다."), + ACTIVE_TEAM_SINGLE_MISSION_SUCCESS("진행 중인 팀별 한번 인증 미션 조회를 완료 했습니다."), + ACTIVE_TEAM_REPEAT_MISSION_SUCCESS("진행 중인 팀별 반복 인증 미션 조회를 완료 했습니다."), + FINISH_ALL_MISSION_SUCCESS("종료된 미션 조회를 완료하였습니다."), + MISSION_ARCHIVE_BY_TEAM("팀별 미션 인증물 사진 조회를 완료했습니다."), + GET_MY_TEAM_LIST_SUCCESS("내가 속한 팀을 모두 조회했습니다."); + + private final String message; + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/entity/MissionArchive.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/entity/MissionArchive.java new file mode 100644 index 00000000..558a6089 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/entity/MissionArchive.java @@ -0,0 +1,80 @@ +package com.moing.backend.domain.missionArchive.domain.entity; + + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class MissionArchive extends BaseTimeEntity { // 1회 미션을 저장 하는 저장소 + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "missionArchive_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mission_id") + private Mission mission; + + @Enumerated(value = EnumType.STRING) + private MissionArchiveStatus status; + + @Column(nullable = false, columnDefinition="TEXT", length = 4000) + private String archive; //링크, 글, 사진 뭐든 가능 + + private Long count; // 횟수 + + @Column(nullable = true, columnDefinition="TEXT", length = 1000) + private String contents; + + @OneToMany(mappedBy = "missionArchive", cascade = CascadeType.REMOVE) + private List heartList = new ArrayList<>(); + + //반정규화 + private Long commentNum; + + public void updateArchive(MissionArchiveReq missionArchiveReq) { + this.archive = missionArchiveReq.getArchive(); + this.status = MissionArchiveStatus.valueOf(missionArchiveReq.getStatus()); + } + + public void updateArchive(String archive) { + this.archive = archive; + } + + public void updateCount(Long count) { + this.count = count; + } + + public void incrComNum() { + this.commentNum++; + } + + public void decrComNum() { + this.commentNum--; + } + + + public String getWriterNickName(){ + return member.getNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/entity/MissionArchiveStatus.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/entity/MissionArchiveStatus.java new file mode 100644 index 00000000..2fe72b45 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/entity/MissionArchiveStatus.java @@ -0,0 +1,5 @@ +package com.moing.backend.domain.missionArchive.domain.entity; + +public enum MissionArchiveStatus { + INCOMPLETE,COMPLETE,SKIP +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepository.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepository.java new file mode 100644 index 00000000..81de1683 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepository.java @@ -0,0 +1,45 @@ +package com.moing.backend.domain.missionArchive.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.RepeatMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchivePhotoRes; +import com.moing.backend.domain.missionArchive.application.dto.res.MyArchiveStatus; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +@Repository +public interface MissionArchiveCustomRepository { + Optional> findSingleMissionInComplete(Long memberId, Long teamId, MissionStatus status,OrderCondition orderCondition); + Optional> findSingleMissionComplete(Long memberId, Long teamId, MissionStatus status,OrderCondition orderCondition); + Optional> findMyArchives(Long memberId,Long missionId); + Optional> findOneMyArchives(Long memberId,Long missionId,Long count); + + Optional> findOthersArchives(Long memberId, Long missionId) ; + + Optional findDonePeopleBySingleMissionId(Long missionId); + Optional findDonePeopleByRepeatMissionId(Long missionId); + Optional findMyDoneCountByMissionId(Long missionId,Long memberId); + + Optional> findRepeatMissionArchivesByMemberId(Long memberId, Long teamId, MissionStatus status); + + Optional> findFinishMissionsByStatus(Long memberId, Long teamId); + + Optional> findTop5ArchivesByTeam(List teamIds); + + Boolean findMyArchivesToday(Long memberId,Long missionId); + + Optional> findHavingRemainMissionsByQuerydsl() ; + + + MyArchiveStatus findMissionStatusById(Long memberId, Long missionId, Long teamId); + + Long getCountsByMissionId(Long missionId); + + Long getTodayMissionArchives(); + Long getYesterdayMissionArchives(); +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepositoryImpl.java new file mode 100644 index 00000000..8d751358 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepositoryImpl.java @@ -0,0 +1,525 @@ +package com.moing.backend.domain.missionArchive.domain.repository; + +import com.moing.backend.domain.block.domain.repository.BlockRepositoryUtils; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.RepeatMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchivePhotoRes; +import com.moing.backend.domain.missionArchive.application.dto.res.MyArchiveStatus; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchiveStatus; +import com.moing.backend.domain.missionRead.domain.repository.MissionReadRepositoryUtils; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.extern.slf4j.Slf4j; + +import javax.persistence.EntityManager; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.mission.domain.entity.QMission.mission; +import static com.moing.backend.domain.missionArchive.domain.entity.QMissionArchive.missionArchive; +//import static com.moing.backend.domain.missionState.domain.entity.QMissionState.missionState; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; + +@Slf4j +public class MissionArchiveCustomRepositoryImpl implements MissionArchiveCustomRepository { + + private final JPAQueryFactory queryFactory; + + public MissionArchiveCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Optional> findSingleMissionInComplete(Long memberId, Long teamId, MissionStatus status, + OrderCondition orderCondition) { + + BooleanExpression isReadExpression = MissionReadRepositoryUtils.isMissionReadByMemberIdAndTeamId(memberId, teamId); + + OrderSpecifier[] orderSpecifiers = createOrderSpecifier(orderCondition); + return Optional.ofNullable(queryFactory + .select(Projections.constructor(SingleMissionBoardRes.class, + mission.id, + mission.dueTo.stringValue(), + mission.title, + mission.status.stringValue(), + mission.type.stringValue(), + isReadExpression.as("isRead") + )) + .from(mission) + .where(mission.notIn + (JPAExpressions + .select(missionArchive.mission) + .from(missionArchive) + .where(missionArchive.member.memberId.eq(memberId), + missionArchive.mission.team.teamId.eq(teamId), + missionArchive.mission.type.eq(MissionType.ONCE), + missionArchive.mission.status.eq(MissionStatus.SUCCESS) + .or(missionArchive.mission.status.eq(MissionStatus.ONGOING)) + )), + mission.type.eq(MissionType.ONCE), + mission.status.eq(MissionStatus.ONGOING).or(mission.status.eq(MissionStatus.WAIT)), + mission.team.teamId.eq(teamId)) + .orderBy(orderSpecifiers).fetch()); + + + + } + @Override + public Optional> findSingleMissionComplete(Long memberId, Long teamId, MissionStatus status, + OrderCondition orderCondition) { + BooleanExpression isReadExpression = MissionReadRepositoryUtils.isMissionReadByMemberIdAndTeamId(memberId, teamId); + + OrderSpecifier[] orderSpecifiers = createOrderSpecifier(orderCondition); + return Optional.ofNullable(queryFactory + .select(Projections.constructor(SingleMissionBoardRes.class, + mission.id, + mission.dueTo.stringValue(), + mission.title, + missionArchive.status.stringValue(), + mission.type.stringValue(), + isReadExpression.as("isRead") + )) + .from(mission) + .join(mission.missionArchiveList,missionArchive) + .on(missionArchive.member.memberId.eq(memberId)) + .where( + mission.team.teamId.eq(teamId), + mission.type.eq(MissionType.ONCE), + mission.status.eq(MissionStatus.ONGOING) + ) + .orderBy(orderSpecifiers).fetch()); + + + } + + private OrderSpecifier[] createOrderSpecifier(OrderCondition orderCondition) { + + List orderSpecifiers = new ArrayList<>(); + + if (orderCondition.equals(OrderCondition.DUETO)) { + orderSpecifiers.add(new OrderSpecifier(Order.ASC, mission.dueTo)); + } else if (orderCondition.equals(OrderCondition.CREATED)) { + orderSpecifiers.add(new OrderSpecifier(Order.ASC, missionArchive.createdDate)); + } + return orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]); + } + + @Override + public Optional> findOneMyArchives(Long memberId,Long missionId,Long count) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.ofNullable(queryFactory + .select(missionArchive) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + missionArchive.member.memberId.eq(memberId), + missionArchive.count.eq(count), + + missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .orderBy(missionArchive.createdDate.desc()) + .fetch() + + ); + } + + @Override + public Optional> findMyArchives(Long memberId,Long missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.ofNullable(queryFactory + .select(missionArchive) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + missionArchive.member.memberId.eq(memberId), + (missionArchive.mission.type.eq(MissionType.REPEAT) + .and(missionArchive.mission.status.eq(MissionStatus.ONGOING)).and(dateInRange)) + .or(missionArchive.mission.type.eq(MissionType.REPEAT) + .and(missionArchive.mission.status.eq(MissionStatus.END))) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + + ) + .orderBy(missionArchive.createdDate.desc()) + .fetch() + + ); + } + + + @Override + public Optional> findOthersArchives(Long memberId, Long missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + BooleanExpression blockCondition= BlockRepositoryUtils.blockCondition(memberId, missionArchive.member.memberId); + + return Optional.ofNullable(queryFactory + .select(missionArchive) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + missionArchive.member.memberId.ne(memberId), + missionArchive.status.eq(MissionArchiveStatus.COMPLETE).or(missionArchive.status.eq(MissionArchiveStatus.SKIP)), + blockCondition, + (missionArchive.mission.type.eq(MissionType.REPEAT) + .and(missionArchive.mission.status.eq(MissionStatus.ONGOING)).and(dateInRange)) + .or(missionArchive.mission.type.eq(MissionType.REPEAT) + .and(missionArchive.mission.status.eq(MissionStatus.END))) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + + ) + .orderBy(missionArchive.createdDate.desc()) + .fetch() + ); + } + + + @Override + public Optional findDonePeopleBySingleMissionId(Long missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.of(queryFactory + .select(missionArchive) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + + missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .groupBy(missionArchive.member) + .fetchCount() + + ); + } + @Override + public Optional findDonePeopleByRepeatMissionId(Long missionId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.of(queryFactory + .select(missionArchive) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + + missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .groupBy(missionArchive.member,missionArchive.mission.number) + .having(missionArchive.count().goe(missionArchive.mission.number)) + .fetchCount() + + ); + } + + @Override + public Optional findMyDoneCountByMissionId(Long missionId, Long memberId) { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + return Optional.ofNullable(queryFactory + .select(missionArchive.count()) + .from(missionArchive) + .where( + missionArchive.mission.id.eq(missionId), + missionArchive.member.memberId.eq(memberId), + missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .fetchFirst() + ); + } + + + @Override + public Optional> findRepeatMissionArchivesByMemberId(Long memberId, Long teamId, MissionStatus status) { + + BooleanExpression isReadExpression = MissionReadRepositoryUtils.isMissionReadByMemberIdAndTeamId(memberId, teamId); + + + BooleanExpression dateInRange = createRepeatTypeConditionByState(); + return Optional.ofNullable(queryFactory + .select(Projections.constructor(RepeatMissionBoardRes.class, + mission.id, + mission.title, +// missionState.count().coalesce(0L).as("done"), + missionArchive.count(), + mission.number, + mission.way.stringValue(), + mission.status.stringValue(), + isReadExpression.as("isRead") + )) + .from(mission) + .leftJoin(missionArchive) + .on(missionArchive.mission.eq(mission), + missionArchive.member.memberId.eq(memberId), + dateInRange + ) + .where( + mission.team.teamId.eq(teamId), + mission.type.eq(MissionType.REPEAT), + mission.status.eq(MissionStatus.ONGOING).or(mission.status.eq(MissionStatus.WAIT)) + ) + .groupBy(mission.id,mission.number) +// .having(missionState.count().lt(mission.number)) // HAVING 절을 사용하여 조건 적용 + .orderBy(missionArchive.count().desc()) + .fetch()); + + } + + @Override + public Optional> findFinishMissionsByStatus(Long memberId, Long teamId) { + + Expression dueToString = Expressions.stringTemplate("DATE_FORMAT({0}, '%Y-%m-%d %H:%i.%s')", mission.dueTo); + Expression status = Expressions.stringTemplate(String.valueOf(missionArchive.status)); + Expression type = Expressions.stringTemplate(String.valueOf(mission.type)); + + + return Optional.ofNullable(queryFactory + .selectDistinct(Projections.constructor(FinishMissionBoardRes.class, + mission.id, + mission.dueTo.stringValue(), + mission.title, + missionArchive.status.stringValue().coalesce("FAIL").as("status"), + mission.type.stringValue(), + mission.way.stringValue() + )) + .from(mission) + .leftJoin(mission.missionArchiveList,missionArchive) + .on(missionArchive.member.memberId.eq(memberId)) + .where( + mission.team.teamId.eq(teamId), + mission.status.eq(MissionStatus.SUCCESS).or(mission.status.eq(MissionStatus.END)) + ).orderBy(mission.lastModifiedDate.desc()) + .fetch() + ); + } + + + public Optional> findTop5ArchivesByTeam(List teamIds) { + List queryResults = queryFactory + .select(missionArchive.mission.team.teamId, missionArchive.archive) + .from(missionArchive) + .where(missionArchive.mission.team.teamId.in(teamIds), + missionArchive.mission.way.eq(MissionWay.PHOTO), + missionArchive.status.eq(MissionArchiveStatus.COMPLETE), + missionArchive.archive.ne("https://modagbul.s3.ap-northeast-2.amazonaws.com/reportImage.png"), + missionArchive.archive.ne("https://mo-ing.s3.ap-northeast-2.amazonaws.com/reportImage.png")) + .orderBy(missionArchive.createdDate.desc()) + .limit(14) + .fetch(); + + List resultDTOs = new ArrayList<>(); + + for (Tuple tuple : queryResults) { + Long teamId = tuple.get(missionArchive.mission.team.teamId); + String photo = tuple.get(missionArchive.archive); + + // Check if a TeamPhotoDTO with the same teamId already exists in the list + MissionArchivePhotoRes existingDTO = resultDTOs.stream() + .filter(dto -> dto.getTeamId().equals(teamId)) + .findFirst() + .orElse(null); + + if (existingDTO != null) { + existingDTO.getPhoto().add(photo); + } else { + List photoList = new ArrayList<>(); + photoList.add(photo); + resultDTOs.add(new MissionArchivePhotoRes(teamId, photoList)); + } + } + + return Optional.ofNullable(resultDTOs); + } + + + public Boolean findMyArchivesToday(Long memberId, Long missionId) { + LocalDateTime today = LocalDateTime.now(); + LocalDateTime startOfToday = today.withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfToday = today.withHour(23).withMinute(59).withSecond(59).withNano(999999999); + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + long count = queryFactory + .selectFrom(missionArchive) + .where( + missionArchive.member.memberId.eq(memberId), + missionArchive.mission.id.eq(missionId), + missionArchive.createdDate.between(startOfToday, endOfToday), // createdDate와 오늘의 시작과 끝을 비교, + + missionArchive.mission.type.eq(MissionType.REPEAT).and(dateInRange) + .or(missionArchive.mission.type.eq(MissionType.ONCE)) + ) + .fetchCount(); + + return count > 0; + } + + + private BooleanExpression createRepeatTypeConditionByArchive() { + LocalDate now = LocalDate.now(); + DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY; + LocalDate startOfWeek = now.with(TemporalAdjusters.previousOrSame(firstDayOfWeek)); + LocalDate endOfWeek = startOfWeek.plusDays(6); + + BooleanExpression dateInRange = missionArchive.createdDate.goe(startOfWeek.atStartOfDay()) + .and(missionArchive.createdDate.loe(endOfWeek.atStartOfDay().plusDays(1).minusNanos(1))); + + // 조건이 MissionType.REPEAT 인 경우에만 날짜 범위 조건 적용 + return dateInRange.and(dateInRange); + } + + private BooleanExpression createRepeatTypeConditionByState() { + LocalDate now = LocalDate.now(); + DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY; + LocalDate startOfWeek = now.with(TemporalAdjusters.previousOrSame(firstDayOfWeek)); + LocalDate endOfWeek = startOfWeek.plusDays(6); + + BooleanExpression dateInRange = missionArchive.createdDate.goe(startOfWeek.atStartOfDay()) + .and(missionArchive.createdDate.loe(endOfWeek.atStartOfDay().plusDays(1).minusNanos(1))); + + // 조건이 MissionType.REPEAT 인 경우에만 날짜 범위 조건 적용 + return dateInRange.and(dateInRange); + } + +// +// @Query(value = "SELECT distinct COALESCE(tmSub.fcm_token,'undef') as fcmToken, tmSub.member_id as memberId " + +// "FROM (SELECT distinct COALESCE(tm.member_id, 0) AS member_id, t.team_id, me.fcm_token " + +// "FROM mission m " + +// "LEFT JOIN team t ON m.team_id = t.team_id " + +// "LEFT JOIN team_member tm ON t.team_id = tm.team_id AND tm.is_deleted = 'False' " + +// "LEFT JOIN member me on tm.member_id = me.member_id) tmSub " + +// "LEFT JOIN mission m ON NOT (m.status = 'END' OR m.status = 'SUCCESS') and m.team_id = tmSub.team_id " + +// "LEFT JOIN mission_archive ms ON m.mission_id = ms.mission_id and ms.member_id = tmSub.member_id " + +// "GROUP BY tmSub.member_id, m.mission_id, m.number " + +// "HAVING COUNT(ms.mission_archive_id) < m.number", nativeQuery = true +// ) + + @Override + public Optional> findHavingRemainMissionsByQuerydsl() { + + BooleanExpression dateInRange = createRepeatTypeConditionByArchive(); + + return Optional.ofNullable(queryFactory + .select(teamMember.member).distinct() + .from(teamMember) + .join(mission) + .on(teamMember.team.eq(mission.team), + teamMember.team.isDeleted.ne(true), + ((mission.status.eq(MissionStatus.ONGOING).or(mission.status.eq(MissionStatus.WAIT))) + .and(mission.type.eq(MissionType.ONCE))) + .or(mission.status.eq(MissionStatus.ONGOING).and(mission.type.eq(MissionType.REPEAT))) + ) + .leftJoin(missionArchive) + .on(missionArchive.mission.eq(mission), + missionArchive.member.eq(teamMember.member), + ((mission.type.eq(MissionType.REPEAT).and(dateInRange)) + .or(mission.type.eq(MissionType.ONCE))) + ) + .groupBy(teamMember.member,mission,mission.number) + .having(missionArchive.count().lt(mission.number), + teamMember.member.isDeleted.ne(true)) + .fetch()); + + + } + + @Override + public MyArchiveStatus findMissionStatusById(Long memberId, Long missionId, Long teamId) { + + return queryFactory + .select(Projections.constructor(MyArchiveStatus.class, + mission.status.eq(MissionStatus.END), + new CaseBuilder() + .when(mission.status.eq(MissionStatus.WAIT).or(mission.status.eq(MissionStatus.ONGOING))) + .then(missionArchive.status.stringValue().coalesce(mission.status.stringValue())) + .otherwise(missionArchive.status.stringValue().coalesce("FAIL")) + + )).distinct() + .from(mission) + .leftJoin(missionArchive) + .on( + missionArchive.mission.eq(mission), + missionArchive.member.memberId.eq(memberId)) + .where( + mission.team.teamId.eq(teamId), + mission.id.eq(missionId) + ) + .fetchFirst(); + + } + + public Long getCountsByMissionId(Long missionId) { + + BooleanExpression repeatTypeCondition = createRepeatTypeConditionByArchive(); + // 기본 조건 + BooleanExpression baseCondition = missionArchive.mission.id.eq(missionId); + // 조건 적용 + BooleanExpression finalCondition = baseCondition.and(repeatTypeCondition); + + return (long) queryFactory + .select(missionArchive) + .from(missionArchive) + .where(finalCondition) + .fetch().size(); + } + + @Override + public Long getTodayMissionArchives() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long todayMissionArchives = queryFactory + .selectFrom(missionArchive) + .where(missionArchive.createdDate.between(startOfToday, endOfToday)) + .fetchCount(); + + return todayMissionArchives; + } + + @Override + public Long getYesterdayMissionArchives() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long yesterdayMissionArchives = queryFactory + .selectFrom(missionArchive) + .where(missionArchive.createdDate.between(startOfYesterday, startOfToday)) + .fetchCount(); + + return yesterdayMissionArchives; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveRepository.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveRepository.java new file mode 100644 index 00000000..280bf848 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveRepository.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.missionArchive.domain.repository; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import feign.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface MissionArchiveRepository extends JpaRepository,MissionArchiveCustomRepository { + + @Query("select m from MissionArchive as m where m.mission.id = :missionId and m.member.memberId =:memberId order by m.createdDate") + Optional> findArchivesByMissionIdAndMemberId(@Param("memberId") Long memberId, @Param("missionId")Long missionId); + + } diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/OrderCondition.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/OrderCondition.java new file mode 100644 index 00000000..ab250eca --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/repository/OrderCondition.java @@ -0,0 +1,6 @@ +package com.moing.backend.domain.missionArchive.domain.repository; + +public enum OrderCondition { + DUETO,CREATED + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveDeleteService.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveDeleteService.java new file mode 100644 index 00000000..567f8ae9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveDeleteService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.missionArchive.domain.service; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.repository.MissionArchiveRepository; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentDeleteService; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionArchiveDeleteService { + + private final MissionArchiveRepository missionArchiveRepository; + + public void deleteMissionArchive(MissionArchive missionArchive) { + missionArchiveRepository.delete(missionArchive); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveQueryService.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveQueryService.java new file mode 100644 index 00000000..e5edfa07 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveQueryService.java @@ -0,0 +1,127 @@ +package com.moing.backend.domain.missionArchive.domain.service; + +import com.moing.backend.domain.member.domain.repository.MemberRepository; +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.RepeatMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +import com.moing.backend.domain.mission.domain.repository.MissionRepository; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchivePhotoRes; +import com.moing.backend.domain.missionArchive.application.dto.res.MyArchiveStatus; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.repository.MissionArchiveRepository; +import com.moing.backend.domain.missionArchive.exception.NotFoundMissionArchiveException; +import com.moing.backend.domain.missionArchive.domain.repository.OrderCondition; +import com.moing.backend.domain.teamMember.domain.repository.TeamMemberRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionArchiveQueryService { + + private final MissionRepository missionRepository; + private final MissionArchiveRepository missionArchiveRepository; + private final TeamMemberRepository teamMemberRepository; + private final MemberRepository memberRepository; + + public MissionArchive findByMissionArchiveId(Long missionArchiveId) { + return missionArchiveRepository.findById(missionArchiveId).orElseThrow(NotFoundMissionArchiveException::new); + } + + + public List findMyArchive(Long memberId, Long missionId) { + + Optional> optional = missionArchiveRepository.findMyArchives(memberId, missionId); + + if (optional.isPresent() && optional.get().size() == 0) { + return new ArrayList<>(); + } else { + return optional.get(); + } + } + public MissionArchive findOneMyArchive(Long memberId, Long missionId, Long count) { + + List missionArchives = missionArchiveRepository.findMyArchives(memberId, missionId).orElseThrow(NotFoundMissionArchiveException::new); + return missionArchives.stream().filter( m -> m.getCount().equals(count)).findFirst().orElseThrow(NotFoundMissionArchiveException::new); + + } + + public List findOthersArchive(Long memberId, Long missionId) { + return missionArchiveRepository.findOthersArchives(memberId, missionId).orElseThrow(NotFoundMissionArchiveException::new); + } + + + public Boolean isDone(Long memberId, Long missionId) { + Optional> byMemberId = missionArchiveRepository.findArchivesByMissionIdAndMemberId(memberId, missionId); + if (byMemberId.isPresent()) { + return Boolean.TRUE; + } else { + return Boolean.FALSE; + } + } + + /** + * mission.getTeam() 팀의 단일미션 미션 인증 보드 + */ + public List findMySingleMissionArchives(Long memberId, Long teamId, MissionStatus missionStatus) { + + // INCOMPLETE + List incompleteList = missionArchiveRepository.findSingleMissionInComplete(memberId, teamId, missionStatus, OrderCondition.DUETO) + .orElseThrow(NotFoundMissionArchiveException::new); + + List completeList = missionArchiveRepository.findSingleMissionComplete(memberId, teamId, missionStatus, OrderCondition.CREATED) + .orElseThrow(NotFoundMissionArchiveException::new); + + incompleteList.addAll(completeList); + return incompleteList; + } + + public List findMyRepeatMissionArchives(Long memberId, Long teamId, MissionStatus missionStatus) { + return missionArchiveRepository.findRepeatMissionArchivesByMemberId(memberId, teamId, missionStatus).orElseThrow(NotFoundMissionArchiveException::new); + } + + + public Long findDoneSingleArchives(Long missionId) { + return missionArchiveRepository.findDonePeopleBySingleMissionId(missionId).orElseThrow(NotFoundMissionArchiveException::new); + } + public Long findDoneRepeatArchives(Long missionId) { + return missionArchiveRepository.findDonePeopleByRepeatMissionId(missionId).orElseThrow(NotFoundMissionArchiveException::new); + } + + public Long findMyDoneArchives(Long memberId, Long missionId) { + return missionArchiveRepository.findMyDoneCountByMissionId(missionId, memberId).orElseThrow(NotFoundMissionArchiveException::new); + } + + + public List findMyFinishMissions(Long memberId, Long teamId) { + return missionArchiveRepository.findFinishMissionsByStatus(memberId, teamId).orElseThrow(NotFoundMissionArchiveException::new); + } + + public List findTop5ArchivesByTeam(List teamIds) { + return missionArchiveRepository.findTop5ArchivesByTeam(teamIds).orElse(null); + } + + public boolean isAbleToArchiveToday(Long memberId, Long missionId) { + return missionArchiveRepository.findMyArchivesToday(memberId, missionId); + } + + public MyArchiveStatus findMissionStatusById(Long memberId, Long missionId, Long teamId) { + return missionArchiveRepository.findMissionStatusById(memberId, missionId, teamId); + } + + public Long stateCountByMissionId(Long missionId) { + return missionArchiveRepository.getCountsByMissionId(missionId); + } + + public Long getTodayMissionArchives(){ + return missionArchiveRepository.getTodayMissionArchives(); + } + public Long getYesterdayMissionArchives(){ + return missionArchiveRepository.getYesterdayMissionArchives(); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveSaveService.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveSaveService.java new file mode 100644 index 00000000..bf8a53ac --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveSaveService.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.missionArchive.domain.service; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.repository.MissionArchiveRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionArchiveSaveService { + + private final MissionArchiveRepository missionArchiveRepository; + + public MissionArchive save(MissionArchive missionArchive) { + return missionArchiveRepository.save(missionArchive); + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveScheduleQueryService.java b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveScheduleQueryService.java new file mode 100644 index 00000000..3560526b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/domain/service/MissionArchiveScheduleQueryService.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.missionArchive.domain.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveRes; +import com.moing.backend.domain.missionArchive.domain.repository.MissionArchiveRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionArchiveScheduleQueryService { + + + private final MissionArchiveRepository missionArchiveRepository; + + public List getRemainMissionPeople() { + return missionArchiveRepository.findHavingRemainMissionsByQuerydsl().orElseThrow(); + + } + + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/exception/MissionArchiveException.java b/src/main/java/com/moing/backend/domain/missionArchive/exception/MissionArchiveException.java new file mode 100644 index 00000000..a6711c3d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/exception/MissionArchiveException.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.missionArchive.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class MissionArchiveException extends ApplicationException { + + protected MissionArchiveException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/exception/NoAccessMissionArchiveException.java b/src/main/java/com/moing/backend/domain/missionArchive/exception/NoAccessMissionArchiveException.java new file mode 100644 index 00000000..8bb4402e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/exception/NoAccessMissionArchiveException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.missionArchive.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoAccessMissionArchiveException extends MissionArchiveException { + public NoAccessMissionArchiveException() { + super(ErrorCode.NO_MORE_ARCHIVE_ERROR, HttpStatus.NOT_FOUND); + + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/exception/NoMoreMissionArchiveException.java b/src/main/java/com/moing/backend/domain/missionArchive/exception/NoMoreMissionArchiveException.java new file mode 100644 index 00000000..cd35fc03 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/exception/NoMoreMissionArchiveException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.missionArchive.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoMoreMissionArchiveException extends MissionArchiveException { + + public NoMoreMissionArchiveException() { + super(ErrorCode.NO_MORE_ARCHIVE_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/exception/NotFoundMissionArchiveException.java b/src/main/java/com/moing/backend/domain/missionArchive/exception/NotFoundMissionArchiveException.java new file mode 100644 index 00000000..521bb472 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/exception/NotFoundMissionArchiveException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.missionArchive.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundMissionArchiveException extends MissionArchiveException { + + public NotFoundMissionArchiveException() { + super(ErrorCode.NOT_FOUND_MISSION_ARCHIVE, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/exception/NotYetMissionArchiveException.java b/src/main/java/com/moing/backend/domain/missionArchive/exception/NotYetMissionArchiveException.java new file mode 100644 index 00000000..25c87401 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/exception/NotYetMissionArchiveException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.missionArchive.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotYetMissionArchiveException extends MissionArchiveException { + + public NotYetMissionArchiveException() { + super(ErrorCode.NOT_YET_MISSION_ARCHIVE, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionArchiveController.java b/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionArchiveController.java new file mode 100644 index 00000000..a321467f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionArchiveController.java @@ -0,0 +1,161 @@ +package com.moing.backend.domain.missionArchive.presentation; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.application.dto.res.GatherRepeatMissionRes; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionArchive.application.dto.res.*; +import com.moing.backend.domain.missionArchive.application.service.*; + + +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveDeleteService; + +import com.moing.backend.domain.missionHeart.application.dto.MissionHeartRes; +import com.moing.backend.domain.missionHeart.application.service.MissionHeartUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +import static com.moing.backend.domain.missionArchive.domain.constant.MissionArchiveResponseMessage.*; + + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team/{teamId}/missions/{missionId}/archive") +public class MissionArchiveController { + + private final MissionArchiveCreateUseCase missionArchiveCreateUseCase; + private final MissionArchiveReadUseCase missionArchiveReadUseCase; + private final MissionArchiveUpdateUseCase missionArchiveUpdateUseCase; + private final MissionArchiveDeleteUseCase missionArchiveDeleteUseCase; + + private final RepeatMissionArchiveReadUseCase repeatMissionArchiveReadUseCase; + private final MissionHeartUseCase missionHeartUseCase; + + + /** + * 미션 인증 하기 + * [POST] {teamId}/missions/{missionId}/archive + * 작성자 : 정승연 + **/ + + @PostMapping() + public ResponseEntity> createArchive(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId, + @Valid @RequestBody MissionArchiveReq missionArchiveReq) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_ARCHIVE_SUCCESS.getMessage(), this.missionArchiveCreateUseCase.createArchive(user.getSocialId(), missionId, missionArchiveReq))); + } + + /** + * 미션 재인증 하기 + * [POST] {teamId}/missions/{missionId}/archive + * 작성자 : 정승연 + **/ + + @PutMapping() + public ResponseEntity> updateArchive(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId, + @RequestBody MissionArchiveReq missionArchiveReq) { + return ResponseEntity.ok(SuccessResponse.create(UPDATE_ARCHIVE_SUCCESS.getMessage(), this.missionArchiveUpdateUseCase.updateArchive(user.getSocialId(), missionId, missionArchiveReq))); + } + + /** + * 미션 인증 취소하기 + * [DELETE] {teamId}/missions/{missionId}/archive + * 작성자 : 정승연 + **/ + + @DeleteMapping("/{count}") + public ResponseEntity> deleteArchive(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId, + @PathVariable("count") Long count) { + return ResponseEntity.ok(SuccessResponse.create(DELETE_ARCHIVE_SUCCESS.getMessage(), this.missionArchiveDeleteUseCase.deleteArchive(user.getSocialId(), missionId,count))); + } + + /** + * 미션 인증 조회 + * [GET] {teamId}/missions/{missionId}/archive + * 작성자 : 정승연 + **/ + + @GetMapping() + public ResponseEntity> getMyArchive(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(READ_MY_ARCHIVE_SUCCESS.getMessage(), this.missionArchiveReadUseCase.getMyArchive(user.getSocialId(), missionId))); + } + + /** + * 모임원 미션 인증 목록 조회 + * [GET] {teamId}/missions/{missionId}/archive/others + * 작성자 : 정승연 + **/ + @GetMapping("/others") + public ResponseEntity>> getOtherPeopleArchives(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(READ_TEAM_ARCHIVE_SUCCESS.getMessage(), this.missionArchiveReadUseCase.getPersonalArchive(user.getSocialId(), missionId))); + } + + + /** + * 미션 인증 좋아요 누르기 + * [POST] {teamId}/missions/{missionId}/archive + * 작성자 : 정승연 + **/ + + @PutMapping("{archiveId}/heart/{missionHeartStatus}") + public ResponseEntity> pushHeart(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId, + @PathVariable("archiveId") Long archiveId, + @PathVariable("missionHeartStatus") String missionHeartStatus) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_ARCHIVE_SUCCESS.getMessage(), this.missionHeartUseCase.pushHeart(user.getSocialId(), archiveId, missionHeartStatus))); + } + + /** + * 인증 성공 인원 조회 + * [GET] {teamId}/missions/{missionId}/archive/status + * 작성자 : 정승연 + **/ + + @GetMapping("/status") + public ResponseEntity> getMissionDoneStatus(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(MISSION_ARCHIVE_PEOPLE_STATUS_SUCCESS.getMessage(), this.missionArchiveReadUseCase.getMissionDoneStatus(missionId))); + } + + + /** + * 반복미션 - 나의 성공 횟수 조회 + * [GET] {teamId}/missions/{missionId}/archive/my-status + * 작성자 : 정승연 + **/ + + @GetMapping("/my-status") + public ResponseEntity> getMyMissionDoneStatus(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(MISSION_ARCHIVE_PEOPLE_STATUS_SUCCESS.getMessage(), this.repeatMissionArchiveReadUseCase.getMyMissionDoneStatus(user.getSocialId(), missionId))); + } + + + @GetMapping("/mission-status") + public ResponseEntity> getMyMissionStatus(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId, + @PathVariable("missionId") Long missionId) { + return ResponseEntity.ok(SuccessResponse.create(MISSION_ARCHIVE_PEOPLE_STATUS_SUCCESS.getMessage(), this.missionArchiveReadUseCase.getMissionArchiveStatus(user.getSocialId(), missionId,teamId))); + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionBoardController.java b/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionBoardController.java new file mode 100644 index 00000000..82fb4ea7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionBoardController.java @@ -0,0 +1,61 @@ +package com.moing.backend.domain.missionArchive.presentation; + +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.RepeatMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.missionArchive.application.service.MissionArchiveBoardUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.moing.backend.domain.missionArchive.domain.constant.MissionArchiveResponseMessage.*; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team/{teamId}/missions/board") +public class MissionBoardController { + + private final MissionArchiveBoardUseCase missionArchiveBoardUseCase; + + + /** + * 단일 인증 조회 + * [GET] {teamId}/missions/board/single + * 작성자 : 정승연 + */ + + @GetMapping("/single") + public ResponseEntity>> getActiveSingleMission(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(ACTIVE_SINGLE_MISSION_SUCCESS.getMessage(), this.missionArchiveBoardUseCase.getActiveSingleMissions(teamId, user.getSocialId()))); + } + + /** + * 반복 미션 인증 조회 + * [GET] {teamId}/missions/board/repeat + * 작성자 : 정승연 + */ + @GetMapping("/repeat") + public ResponseEntity>> getActiveRepeatMission(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(ACTIVE_REPEAT_MISSION_SUCCESS.getMessage(), this.missionArchiveBoardUseCase.getActiveRepeatMissions(teamId, user.getSocialId()))); + } + + /** + * 종료된 인증 조회 + * [POST] {teamId}/missions/board/finish + * 작성자 : 정승연 + */ + + @GetMapping("/finish") + public ResponseEntity>> getFinishAllMission(@AuthenticationPrincipal User user, + @PathVariable("teamId") Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(FINISH_ALL_MISSION_SUCCESS.getMessage(), this.missionArchiveBoardUseCase.getFinishMissions(teamId, user.getSocialId()))); + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionGatherController.java b/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionGatherController.java new file mode 100644 index 00000000..6a6a2694 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionArchive/presentation/MissionGatherController.java @@ -0,0 +1,94 @@ +package com.moing.backend.domain.missionArchive.presentation; + +import com.moing.backend.domain.mission.application.dto.res.GatherRepeatMissionRes; +import com.moing.backend.domain.mission.application.dto.res.GatherSingleMissionRes; +import com.moing.backend.domain.mission.application.service.MissionGatherBoardUseCase; + +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchivePhotoRes; + +import com.moing.backend.domain.missionArchive.application.dto.res.MyTeamsRes; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.moing.backend.domain.missionArchive.domain.constant.MissionArchiveResponseMessage.*; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team") +public class MissionGatherController { + + private final MissionGatherBoardUseCase missionGatherBoardUseCase; + + + /** + * 미션 모아보기 - 단일 미션 + * [GET] my-single + * 작성자 : 정승연 + */ + + @GetMapping("/my-once") + public ResponseEntity>> getMyActiveSingleMission(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(ACTIVE_TEAM_SINGLE_MISSION_SUCCESS.getMessage(), this.missionGatherBoardUseCase.getAllActiveSingleMissions(user.getSocialId()))); + } + + /** + * 미션 모아보기 - 반복 미션 + * [GET] my-repeat + * 작성자 : 정승연 + */ + + @GetMapping("/team-repeat/{teamId}") + public ResponseEntity>> getTeamActiveRepeatMission(@AuthenticationPrincipal User user ,@PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(ACTIVE_TEAM_REPEAT_MISSION_SUCCESS.getMessage(), this.missionGatherBoardUseCase.getTeamActiveRepeatMissions( user.getSocialId(),teamId))); + } + + /** + * 팀별 미션 모아보기 - 단일 미션 + * [GET] my-single + * 작성자 : 정승연 + */ + + @GetMapping("/team-once/{teamId}") + public ResponseEntity>> getTeamActiveSingleMission(@AuthenticationPrincipal User user ,@PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(ACTIVE_SINGLE_MISSION_SUCCESS.getMessage(), this.missionGatherBoardUseCase.getTeamActiveSingleMissions(user.getSocialId(),teamId))); + } + + /** + * 팀별 미션 모아보기 - 반복 미션 + * [GET] my-repeat + * 작성자 : 정승연 + */ + + @GetMapping("/my-repeat") + public ResponseEntity>> getMyActiveRepeatMission(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(ACTIVE_REPEAT_MISSION_SUCCESS.getMessage(), this.missionGatherBoardUseCase.getAllActiveRepeatMissions( user.getSocialId()))); + } + + + @GetMapping("/my-teams") + public ResponseEntity>> getArchivesByTeam(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(MISSION_ARCHIVE_BY_TEAM.getMessage(), this.missionGatherBoardUseCase.getArchivePhotoByTeamRes(user.getSocialId()))); + } + + @GetMapping("/my-teamList") + public ResponseEntity>> getMyTeams(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_MY_TEAM_LIST_SUCCESS.getMessage(), this.missionGatherBoardUseCase.getMyTeams(user.getSocialId()))); + } + + + + + + + + +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/application/mapper/MissionCommentMapper.java b/src/main/java/com/moing/backend/domain/missionComment/application/mapper/MissionCommentMapper.java new file mode 100644 index 00000000..a47b6e68 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/application/mapper/MissionCommentMapper.java @@ -0,0 +1,20 @@ +package com.moing.backend.domain.missionComment.application.mapper; + +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MissionCommentMapper { + public static MissionComment toMissionComment(TeamMember teamMember, MissionArchive missionArchive, CreateCommentRequest createCommentRequest, boolean isLeader) { + MissionComment missionComment=new MissionComment(); + missionComment.init(createCommentRequest.getContent(),isLeader); + missionComment.updateMissionArchive(missionArchive); + missionComment.updateTeamMember(teamMember); + return missionComment; + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/missionComment/application/service/CreateMissionCommentUseCase.java b/src/main/java/com/moing/backend/domain/missionComment/application/service/CreateMissionCommentUseCase.java new file mode 100644 index 00000000..7e2a2bbc --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/application/service/CreateMissionCommentUseCase.java @@ -0,0 +1,40 @@ +package com.moing.backend.domain.missionComment.application.service; + +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.comment.application.dto.response.CreateCommentResponse; +import com.moing.backend.domain.missionComment.application.mapper.MissionCommentMapper; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentSaveService; +import com.moing.backend.domain.team.application.service.CheckLeaderUseCase; +import com.moing.backend.global.response.BaseMissionServiceResponse; +import com.moing.backend.global.utils.BaseMissionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CreateMissionCommentUseCase { + + private final MissionCommentSaveService missionCommentSaveService; + private final BaseMissionService baseMissionService; + private final CheckLeaderUseCase checkLeaderUseCase; + private final SendMissionCommentAlarmUseCase sendCommentAlarm; + /** + * 게시글 댓글 생성 + */ + public CreateCommentResponse createBoardComment(String socialId, Long teamId, Long missionArchiveId, CreateCommentRequest createCommentRequest) { + // 1. 미션 게시글 댓글 생성 + BaseMissionServiceResponse data = baseMissionService.getCommonData(socialId, teamId, missionArchiveId); + boolean isLeader = checkLeaderUseCase.isTeamLeader(data.getMember(), data.getTeam()); + MissionComment missionComment = missionCommentSaveService.saveComment(MissionCommentMapper.toMissionComment(data.getTeamMember(), data.getMissionArchive(), createCommentRequest, isLeader)); + // 2. 미션 게시글 댓글 개수 증가 + data.getMissionArchive().incrComNum(); + // 3. 미션 게시글 댓글 알림 + sendCommentAlarm.sendCommentAlarm(data, missionComment); + return new CreateCommentResponse(missionComment.getMissionCommentId()); + } +} + diff --git a/src/main/java/com/moing/backend/domain/missionComment/application/service/DeleteMissionCommentUseCase.java b/src/main/java/com/moing/backend/domain/missionComment/application/service/DeleteMissionCommentUseCase.java new file mode 100644 index 00000000..0c87c5b4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/application/service/DeleteMissionCommentUseCase.java @@ -0,0 +1,39 @@ +package com.moing.backend.domain.missionComment.application.service; + +import com.moing.backend.domain.boardComment.exception.NotAuthByBoardCommentException; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentDeleteService; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentGetService; +import com.moing.backend.global.response.BaseMissionServiceResponse; +import com.moing.backend.global.utils.BaseMissionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class DeleteMissionCommentUseCase { + + private final MissionCommentGetService missionCommentGetService; + private final MissionCommentDeleteService missionCommentDeleteService; + private final BaseMissionService baseMissionService; + + /** + * 게시글 댓글 삭제 + */ + + public void deleteMissionComment(String socialId, Long teamId, Long missionArchiveId, Long boardCommentId){ + // 1. 게시글 댓글 조회 + BaseMissionServiceResponse data = baseMissionService.getCommonData(socialId, teamId, missionArchiveId); + MissionComment missionComment =missionCommentGetService.getComment(boardCommentId); + // 2. 게시글 댓글 작성자만 + if (data.getTeamMember() == missionComment.getTeamMember()) { + // 3. 삭제 + missionCommentDeleteService.deleteComment(missionComment); + // 4. 댓글 개수 줄이기 + data.getMissionArchive().decrComNum(); + } else throw new NotAuthByBoardCommentException(); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/application/service/GetMissionCommentUseCase.java b/src/main/java/com/moing/backend/domain/missionComment/application/service/GetMissionCommentUseCase.java new file mode 100644 index 00000000..b3d5ca70 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/application/service/GetMissionCommentUseCase.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.missionComment.application.service; + +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentGetService; +import com.moing.backend.global.response.BaseMissionServiceResponse; +import com.moing.backend.global.utils.BaseMissionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class GetMissionCommentUseCase { + + private final MissionCommentGetService missionCommentGetService; + private final BaseMissionService baseMissionService; + + /** + * 게시글 댓글 전체 조회 + */ + public GetCommentResponse getBoardCommentAll(String socialId, Long teamId, Long missionArchiveId){ + BaseMissionServiceResponse data = baseMissionService.getCommonData(socialId, teamId, missionArchiveId); + return missionCommentGetService.getCommentAll(missionArchiveId, data.getTeamMember()); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/application/service/SendMissionCommentAlarmUseCase.java b/src/main/java/com/moing/backend/domain/missionComment/application/service/SendMissionCommentAlarmUseCase.java new file mode 100644 index 00000000..ec809470 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/application/service/SendMissionCommentAlarmUseCase.java @@ -0,0 +1,93 @@ +package com.moing.backend.domain.missionComment.application.service; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.history.domain.entity.PagePath; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentGetService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.config.fcm.dto.event.MultiFcmEvent; +import com.moing.backend.global.config.fcm.dto.event.SingleFcmEvent; +import com.moing.backend.global.response.BaseMissionServiceResponse; +import lombok.RequiredArgsConstructor; +import net.minidev.json.JSONObject; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.moing.backend.global.config.fcm.constant.NewCommentUploadMessage.NEW_COMMENT_UPLOAD_MESSAGE; + +@Service +@RequiredArgsConstructor +@Transactional +public class SendMissionCommentAlarmUseCase { + + private final ApplicationEventPublisher eventPublisher; + private final MissionCommentGetService missionCommentGetService; + + public void sendCommentAlarm(BaseMissionServiceResponse response, MissionComment comment) { + Member member = response.getMember(); + Team team = response.getTeam(); + MissionArchive missionArchive = response.getMissionArchive(); + Mission mission = missionArchive.getMission(); + + Optional> newUploadInfos = missionCommentGetService.getNewUploadInfo(member.getMemberId(), missionArchive.getId()); + String title = NEW_COMMENT_UPLOAD_MESSAGE.title(comment.getContent()); + String body = NEW_COMMENT_UPLOAD_MESSAGE.body(member.getNickName(), mission.getTitle()); + + sendMissionCommentWriter(mission, missionArchive, title, body, team, newUploadInfos); + sendMissionWriter(missionArchive, mission, member, title, body, team, newUploadInfos); + } + private void sendMissionWriter(MissionArchive missionArchive, Mission mission, + Member member, String title, String body, Team team, + Optional> newUploadInfos) { + Member receiver = missionArchive.getMember(); + if (checkMemberWriter(receiver, member, newUploadInfos)) { + eventPublisher.publishEvent(new SingleFcmEvent(receiver, title, body, createIdInfo(team.getTeamId(), mission.getId(), missionArchive.getId(),mission.getType()), team.getName(), AlarmType.COMMENT, PagePath.MISSION_PATH.getValue(), receiver.isCommentPush())); + } + } + + private void sendMissionCommentWriter(Mission mission, MissionArchive missionArchive, + String title, String body, Team team, + Optional> newUploadInfos) { + Optional> memberIdAndTokensByPush = AlarmHistoryMapper.getNewUploadPushInfo(newUploadInfos); + Optional> memberIdAndTokensBySave = AlarmHistoryMapper.getNewUploadSaveInfo(newUploadInfos); + + eventPublisher.publishEvent(new MultiFcmEvent(title, body, memberIdAndTokensByPush, memberIdAndTokensBySave, createIdInfo(team.getTeamId(), mission.getId(), missionArchive.getId(),mission.getType()), team.getName(), AlarmType.COMMENT, PagePath.MISSION_PATH.getValue())); + } + + + private boolean checkMemberWriter(Member missionWriter, Member commentWriter, Optional> newUploadInfos) { + // 댓글 작성자와 미션 게시글 작성자가 동일한 경우 알림을 보내지 않는다. + if (Objects.equals(missionWriter.getMemberId(), commentWriter.getMemberId())) { + return false; + } + + // newUploadInfos 리스트에 missionWriter memberId가 없는 경우만 알림을 보낸다. + // 리스트가 비어있거나, missionWriter의 memberId가 리스트에 없으면 true를 반환. + return newUploadInfos + .map(infos -> infos.stream().noneMatch(info -> info.getMemberId().equals(missionWriter.getMemberId()))) + .orElse(true); + } + + + private String createIdInfo(Long teamId, Long missionId, Long missionArchiveId, MissionType missionType) { + JSONObject jo = new JSONObject(); + jo.put("missionArchiveId", missionArchiveId); + jo.put("teamId", teamId); + jo.put("missionId", missionId); + jo.put("type", "COMMENT_MISSION"); + jo.put("isRepeated", missionType.equals(MissionType.REPEAT)); + return jo.toJSONString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/entity/MissionComment.java b/src/main/java/com/moing/backend/domain/missionComment/domain/entity/MissionComment.java new file mode 100644 index 00000000..2cc87f50 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/entity/MissionComment.java @@ -0,0 +1,52 @@ +package com.moing.backend.domain.missionComment.domain.entity; + +import com.moing.backend.domain.comment.domain.entity.Comment; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class MissionComment extends Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "mission_comment_id") + private Long missionCommentId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_member_id") + private TeamMember teamMember; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mission_archive_id") + private MissionArchive missionArchive; + + /** + * 연관관계 매핑 + */ + public void updateMissionArchive(MissionArchive missionArchive) { + this.missionArchive=missionArchive; + } + + public void updateTeamMember(TeamMember teamMember) { + this.teamMember = teamMember; + } + + public void init(String content, boolean isLeader){ + this.content=content; + this.isLeader=isLeader; + } + + public String getWriterNickName(){ + return teamMember.getMemberNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentCustomRepository.java b/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentCustomRepository.java new file mode 100644 index 00000000..253b0654 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentCustomRepository.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.missionComment.domain.repository; + +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; + +import java.util.List; +import java.util.Optional; + +public interface MissionCommentCustomRepository { + GetCommentResponse findMissionCommentAll(Long missionArchiveId, TeamMember teamMember); + + Optional> findNewUploadInfo(Long memberId, Long missionArchiveId); +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentCustomRepositoryImpl.java new file mode 100644 index 00000000..9e295ee7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentCustomRepositoryImpl.java @@ -0,0 +1,86 @@ +package com.moing.backend.domain.missionComment.domain.repository; + +import com.moing.backend.domain.block.domain.repository.BlockRepositoryUtils; +import com.moing.backend.domain.comment.application.dto.response.CommentBlocks; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.comment.application.dto.response.QCommentBlocks; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.teamMember.domain.entity.QTeamMember; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.member.domain.entity.QMember.member; +import static com.moing.backend.domain.missionComment.domain.entity.QMissionComment.missionComment; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; + +public class MissionCommentCustomRepositoryImpl implements MissionCommentCustomRepository{ + + private final JPAQueryFactory queryFactory; + + public MissionCommentCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + + @Override + public GetCommentResponse findMissionCommentAll(Long missionArchiveId, TeamMember teamMember) { + + BooleanExpression blockCondition = BlockRepositoryUtils.blockCondition(teamMember.getTeamMemberId(), missionComment.teamMember.member.memberId); + + List commentBlocks = queryFactory + .select(new QCommentBlocks( + missionComment.missionCommentId, + missionComment.content, + missionComment.teamMember.member.nickName, + missionComment.isLeader, + missionComment.teamMember.member.profileImage, + ExpressionUtils.as(JPAExpressions + .selectOne() + .from(QTeamMember.teamMember) + .where(QTeamMember.teamMember.eq(teamMember) + .and(QTeamMember.teamMember.eq(missionComment.teamMember))) + .exists(), "isWriter"), + missionComment.teamMember.isDeleted, + missionComment.createdDate, + missionComment.teamMember.member.memberId)) + .from(missionComment) + .leftJoin(missionComment.teamMember, QTeamMember.teamMember) + .leftJoin(missionComment.teamMember.member, member) + .where(missionComment.missionArchive.id.eq(missionArchiveId) + .and(blockCondition)) + .orderBy(missionComment.createdDate.asc()) + .fetch(); + + return new GetCommentResponse(commentBlocks); + } + + @Override + public Optional> findNewUploadInfo(Long memberId, Long missionArchiveId) { + BooleanExpression blockCondition= BlockRepositoryUtils.blockCondition(missionComment.teamMember.member.memberId, memberId); + + List result = queryFactory.select(Projections.constructor(NewUploadInfo.class, + missionComment.teamMember.member.fcmToken, + missionComment.teamMember.member.memberId, + missionComment.teamMember.member.isCommentPush, + missionComment.teamMember.member.isSignOut)) + .distinct() + .from(missionComment) + .leftJoin(missionComment.teamMember, teamMember) + .leftJoin(missionComment.teamMember.member, member) + .where(missionComment.missionArchive.id.eq(missionArchiveId) //게시글의 댓글인데 + .and(missionComment.teamMember.member.memberId.ne(memberId)) //나는 포함 안하고 + .and(missionComment.teamMember.isDeleted.eq(false)) //탈퇴한 사람도 포함 안함 + .and(blockCondition)) + .fetch(); + + return result.isEmpty() ? Optional.empty() : Optional.of(result); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentRepository.java b/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentRepository.java new file mode 100644 index 00000000..160b39a7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/repository/MissionCommentRepository.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.missionComment.domain.repository; + +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface MissionCommentRepository extends JpaRepository, MissionCommentCustomRepository { + + Optional findMissionCommentByMissionCommentId(Long missionCommentId); + + void deleteAllMissionCommentsByMissionArchiveId(Long missionArchiveId); + +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentDeleteService.java b/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentDeleteService.java new file mode 100644 index 00000000..4a9ff9ec --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentDeleteService.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.missionComment.domain.service; + +import com.moing.backend.domain.comment.domain.service.CommentDeleteService; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.repository.MissionCommentRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class MissionCommentDeleteService implements CommentDeleteService { + + private final MissionCommentRepository missionCommentRepository; + @Override + public void deleteComment(MissionComment comment) { + missionCommentRepository.delete(comment); + } + + public void deleteAllCommentByMissionArchive(Long missionArchiveId) { + missionCommentRepository.deleteAllMissionCommentsByMissionArchiveId(missionArchiveId); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentGetService.java b/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentGetService.java new file mode 100644 index 00000000..501dfe2b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentGetService.java @@ -0,0 +1,34 @@ +package com.moing.backend.domain.missionComment.domain.service; + +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.comment.domain.service.CommentGetService; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.repository.MissionCommentRepository; +import com.moing.backend.domain.missionComment.exception.NotFoundByMissionCommentIdException; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; +@DomainService +@RequiredArgsConstructor +public class MissionCommentGetService implements CommentGetService { + private final MissionCommentRepository missionCommentRepository; + + @Override + public MissionComment getComment(Long commentId) { + return missionCommentRepository.findMissionCommentByMissionCommentId(commentId).orElseThrow(NotFoundByMissionCommentIdException::new); + } + + @Override + public GetCommentResponse getCommentAll(Long missionArchiveId, TeamMember teamMember) { + return missionCommentRepository.findMissionCommentAll(missionArchiveId, teamMember); + } + + @Override + public Optional> getNewUploadInfo(Long memberId, Long missionArchiveId) { + return missionCommentRepository.findNewUploadInfo(memberId, missionArchiveId); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentSaveService.java b/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentSaveService.java new file mode 100644 index 00000000..a62e2e5a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/domain/service/MissionCommentSaveService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.missionComment.domain.service; + +import com.moing.backend.domain.comment.domain.service.CommentSaveService; +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.repository.MissionCommentRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class MissionCommentSaveService implements CommentSaveService { + + private final MissionCommentRepository missionCommentRepository; + + @Override + public MissionComment saveComment(MissionComment comment) { + return missionCommentRepository.save(comment); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/exception/MissionCommentException.java b/src/main/java/com/moing/backend/domain/missionComment/exception/MissionCommentException.java new file mode 100644 index 00000000..3079b682 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/exception/MissionCommentException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.missionComment.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class MissionCommentException extends ApplicationException { + protected MissionCommentException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/missionComment/exception/NotAuthByMissionCommentException.java b/src/main/java/com/moing/backend/domain/missionComment/exception/NotAuthByMissionCommentException.java new file mode 100644 index 00000000..04e9b114 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/exception/NotAuthByMissionCommentException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.missionComment.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotAuthByMissionCommentException extends MissionCommentException { + public NotAuthByMissionCommentException() { + super(ErrorCode.NOT_AUTH_BY_MISSION_COMMENT_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/exception/NotFoundByMissionCommentIdException.java b/src/main/java/com/moing/backend/domain/missionComment/exception/NotFoundByMissionCommentIdException.java new file mode 100644 index 00000000..94c9fcae --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/exception/NotFoundByMissionCommentIdException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.missionComment.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundByMissionCommentIdException extends MissionCommentException { + public NotFoundByMissionCommentIdException() { + super(ErrorCode.NOT_FOUND_BY_MISSION_COMMENT_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionComment/presentation/MissionCommentController.java b/src/main/java/com/moing/backend/domain/missionComment/presentation/MissionCommentController.java new file mode 100644 index 00000000..21b3ac18 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionComment/presentation/MissionCommentController.java @@ -0,0 +1,69 @@ +package com.moing.backend.domain.missionComment.presentation; + +import com.moing.backend.domain.boardComment.presentattion.constant.BoardCommentResponseMessage; +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.comment.application.dto.response.CreateCommentResponse; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.missionComment.application.service.CreateMissionCommentUseCase; +import com.moing.backend.domain.missionComment.application.service.DeleteMissionCommentUseCase; +import com.moing.backend.domain.missionComment.application.service.GetMissionCommentUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +import static com.moing.backend.domain.boardComment.presentattion.constant.BoardCommentResponseMessage.GET_BOARD_COMMENT_ALL_SUCCESS; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/{teamId}/{missionArchiveId}/mcomment") +public class MissionCommentController { + + private final CreateMissionCommentUseCase createMissionCommentUseCase; + private final DeleteMissionCommentUseCase deleteMissionCommentUseCase; + private final GetMissionCommentUseCase getMissionCommentUseCase; + + /** + * 댓글 생성 + * [POST] api/{teamId}/{missionArchiveId}/comment + * 작성자 : 김민수 + */ + @PostMapping + public ResponseEntity> createMissionComment(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long missionArchiveId, + @Valid @RequestBody CreateCommentRequest createCommentRequest) { + return ResponseEntity.ok(SuccessResponse.create(BoardCommentResponseMessage.CREATE_BOARD_COMMENT_SUCCESS.getMessage(), this.createMissionCommentUseCase.createBoardComment(user.getSocialId(), teamId, missionArchiveId, createCommentRequest))); + } + + /** + * 댓글 삭제 + * [DELETE] api/{teamId}/{missionArchiveId}/comment/{commentId} + * 작성자 : 김민수 + */ + @DeleteMapping("/{commentId}") + public ResponseEntity deleteMissionComment(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long missionArchiveId, + @PathVariable Long commentId) { + this.deleteMissionCommentUseCase.deleteMissionComment(user.getSocialId(), teamId, missionArchiveId, commentId); + return ResponseEntity.ok(SuccessResponse.create(BoardCommentResponseMessage.DELETE_BOARD_COMMENT_SUCCESS.getMessage())); + } + + + /** + * 댓글 전체 조회 + * [GET] api/{teamId}/{missionArchiveId}/mcomment + * 작성자 : 김민수 + */ + @GetMapping + public ResponseEntity> getMissionCommentAll(@AuthenticationPrincipal User user, + @PathVariable Long teamId, + @PathVariable Long missionArchiveId) { + return ResponseEntity.ok(SuccessResponse.create(GET_BOARD_COMMENT_ALL_SUCCESS.getMessage(), this.getMissionCommentUseCase.getBoardCommentAll(user.getSocialId(), teamId, missionArchiveId))); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/application/dto/MissionHeartRes.java b/src/main/java/com/moing/backend/domain/missionHeart/application/dto/MissionHeartRes.java new file mode 100644 index 00000000..8f403439 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/application/dto/MissionHeartRes.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.missionHeart.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class MissionHeartRes { + private Long missionArchiveId; + private String missionHeartStatus; + private int hearts; +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/application/mapper/MissionHeartMapper.java b/src/main/java/com/moing/backend/domain/missionHeart/application/mapper/MissionHeartMapper.java new file mode 100644 index 00000000..e33f3575 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/application/mapper/MissionHeartMapper.java @@ -0,0 +1,30 @@ +package com.moing.backend.domain.missionHeart.application.mapper; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionHeart.application.dto.MissionHeartRes; +import com.moing.backend.domain.missionHeart.domain.constant.MissionHeartStatus; +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.global.annotation.Mapper; + +@Mapper +public class MissionHeartMapper { + + public static MissionHeart mapToMissionHeart(Long memberId, MissionArchive archiveId, MissionHeartStatus missionHeartStatus) { + return MissionHeart.builder() + .pushMemberId(memberId) + .missionArchive(archiveId) + .heartStatus(missionHeartStatus) + .build(); + } + + public static MissionHeartRes mapToMissionHeartRes(MissionHeart missionHeart) { + return MissionHeartRes.builder() + .missionArchiveId(missionHeart.getMissionArchive().getId()) + .missionHeartStatus(missionHeart.getHeartStatus().name()) + .hearts((int) missionHeart.getMissionArchive().getHeartList().stream() + .filter(heart -> heart.getHeartStatus().equals( MissionHeartStatus.True)) + .filter(heart -> heart.getId().equals( missionHeart.getId()))// heartStatus가 true인 요소만 필터링 + .count()) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/application/service/MissionHeartUseCase.java b/src/main/java/com/moing/backend/domain/missionHeart/application/service/MissionHeartUseCase.java new file mode 100644 index 00000000..1b296ce7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/application/service/MissionHeartUseCase.java @@ -0,0 +1,49 @@ +package com.moing.backend.domain.missionHeart.application.service; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.missionHeart.application.dto.MissionHeartRes; +import com.moing.backend.domain.missionHeart.application.mapper.MissionHeartMapper; +import com.moing.backend.domain.missionHeart.domain.constant.MissionHeartStatus; +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.domain.missionHeart.domain.service.MissionHeartQueryService; +import com.moing.backend.domain.missionHeart.domain.service.MissionHeartSaveService; +import com.moing.backend.domain.missionHeart.domain.service.MissionHeartUpdateService; +import com.moing.backend.domain.missionHeart.exception.NoAccessMissionHeartException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionHeartUseCase { + + private final MissionHeartSaveService missionHeartSaveService; + private final MissionHeartUpdateService missionHeartUpdateService; + private final MissionHeartQueryService missionHeartQueryService; + private final MemberGetService memberGetService; + private final MissionArchiveQueryService missionArchiveQueryService; + + + public MissionHeartRes pushHeart(String socialId,Long archiveId, String status) { + + Long memberId = memberGetService.getMemberBySocialId(socialId).getMemberId(); + MissionArchive archive = missionArchiveQueryService.findByMissionArchiveId(archiveId); + MissionHeart missionHeart = MissionHeartMapper.mapToMissionHeart(memberId, archive, MissionHeartStatus.valueOf(status)); + + if (memberId.equals(missionHeart.getMissionArchive().getMember().getMemberId())){ + throw new NoAccessMissionHeartException(); + } + if(missionHeartQueryService.isAlreadyHeart(memberId, archiveId)) { + return MissionHeartMapper.mapToMissionHeartRes( + missionHeartUpdateService.update(missionHeart)); + } + else{ + return MissionHeartMapper.mapToMissionHeartRes( + missionHeartSaveService.save(missionHeart)); + } + + } +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/constant/MissionHeartStatus.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/constant/MissionHeartStatus.java new file mode 100644 index 00000000..b9501375 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/constant/MissionHeartStatus.java @@ -0,0 +1,5 @@ +package com.moing.backend.domain.missionHeart.domain.constant; + +public enum MissionHeartStatus { + True,False +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/entity/MissionHeart.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/entity/MissionHeart.java new file mode 100644 index 00000000..cecd17db --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/entity/MissionHeart.java @@ -0,0 +1,44 @@ +package com.moing.backend.domain.missionHeart.domain.entity; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionHeart.domain.constant.MissionHeartStatus; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class MissionHeart extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "missionHeart_id") + private Long id; + + private Long pushMemberId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "missionArchiveId") + private MissionArchive missionArchive; + + @Enumerated(EnumType.STRING) + private MissionHeartStatus heartStatus; + + public void updateHeartStatus(MissionHeartStatus heartStatus) { + this.heartStatus = heartStatus; + } + + public void changeByHeart(MissionArchive missionArchive) { + this.missionArchive = missionArchive; + missionArchive.getHeartList().add(this); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartCustomRepository.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartCustomRepository.java new file mode 100644 index 00000000..df82bfb1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartCustomRepository.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.missionHeart.domain.repository; + +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MissionHeartCustomRepository { + + boolean findAlreadyHeart(Long memberId, Long archiveId); + MissionHeart findByMemberIdAndArchiveId(Long memberId, Long archiveId); + + + } diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartCustomRepositoryImpl.java new file mode 100644 index 00000000..2dd9ceb1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartCustomRepositoryImpl.java @@ -0,0 +1,43 @@ +package com.moing.backend.domain.missionHeart.domain.repository; + +import com.moing.backend.domain.missionHeart.application.dto.MissionHeartRes; +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.domain.missionHeart.domain.entity.QMissionHeart; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.util.Optional; + +import static com.moing.backend.domain.missionHeart.domain.entity.QMissionHeart.missionHeart; + +public class MissionHeartCustomRepositoryImpl implements MissionHeartCustomRepository { + + private final JPAQueryFactory queryFactory; + + public MissionHeartCustomRepositoryImpl(EntityManager entityManager) { + this.queryFactory = new JPAQueryFactory(entityManager); + } + + public boolean findAlreadyHeart(Long memberId, Long archiveId) { + + return queryFactory + .selectFrom(missionHeart) + .where( + missionHeart.pushMemberId.eq(memberId), + missionHeart.missionArchive.id.eq(archiveId) + ).fetchCount() > 0; + + } + public MissionHeart findByMemberIdAndArchiveId(Long memberId, Long archiveId) { + + return queryFactory + .selectFrom(missionHeart) + .where( + missionHeart.pushMemberId.eq(memberId), + missionHeart.missionArchive.id.eq(archiveId) + ).fetchFirst(); + + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartRepository.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartRepository.java new file mode 100644 index 00000000..b79e5f97 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/repository/MissionHeartRepository.java @@ -0,0 +1,7 @@ +package com.moing.backend.domain.missionHeart.domain.repository; + +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MissionHeartRepository extends JpaRepository,MissionHeartCustomRepository { +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartQueryService.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartQueryService.java new file mode 100644 index 00000000..8ab7dcac --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartQueryService.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.missionHeart.domain.service; + +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.domain.missionHeart.domain.repository.MissionHeartRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionHeartQueryService { + + private final MissionHeartRepository missionHeartRepository; + + public boolean isAlreadyHeart(Long memberId, Long archiveId) { + return missionHeartRepository.findAlreadyHeart(memberId, archiveId); + } + public MissionHeart findMissionHeartById(Long memberId, Long archiveId) { + return missionHeartRepository.findByMemberIdAndArchiveId(memberId, archiveId); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartSaveService.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartSaveService.java new file mode 100644 index 00000000..e8ab13fc --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartSaveService.java @@ -0,0 +1,21 @@ +package com.moing.backend.domain.missionHeart.domain.service; + +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.domain.missionHeart.domain.repository.MissionHeartCustomRepository; +import com.moing.backend.domain.missionHeart.domain.repository.MissionHeartRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionHeartSaveService { + + private final MissionHeartRepository missionHeartRepository; + + public MissionHeart save(MissionHeart missionHeart) { + return missionHeartRepository.save(missionHeart); + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartUpdateService.java b/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartUpdateService.java new file mode 100644 index 00000000..5340ea15 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/domain/service/MissionHeartUpdateService.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.missionHeart.domain.service; + +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.repository.MissionArchiveRepository; +import com.moing.backend.domain.missionHeart.domain.entity.MissionHeart; +import com.moing.backend.domain.missionHeart.domain.repository.MissionHeartRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@Transactional +@RequiredArgsConstructor +public class MissionHeartUpdateService { + + private final MissionHeartRepository missionHeartRepository; + + public MissionHeart update(MissionHeart missionHeart) { + + MissionHeart updateHeart = missionHeartRepository.findByMemberIdAndArchiveId(missionHeart.getPushMemberId(), missionHeart.getMissionArchive().getId()); + updateHeart.updateHeartStatus(missionHeart.getHeartStatus()); +// updateHeart.changeByHeart(missionHeart.getMissionArchive()); + + return missionHeartRepository.save(updateHeart); + + } + +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/exception/MissionHeartException.java b/src/main/java/com/moing/backend/domain/missionHeart/exception/MissionHeartException.java new file mode 100644 index 00000000..f085efe1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/exception/MissionHeartException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.missionHeart.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class MissionHeartException extends ApplicationException { + protected MissionHeartException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionHeart/exception/NoAccessMissionHeartException.java b/src/main/java/com/moing/backend/domain/missionHeart/exception/NoAccessMissionHeartException.java new file mode 100644 index 00000000..01fdcde8 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionHeart/exception/NoAccessMissionHeartException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.missionHeart.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NoAccessMissionHeartException extends MissionHeartException { + public NoAccessMissionHeartException() { + super(ErrorCode.NO_ACCESS_HEART_FOR_ME, HttpStatus.NOT_FOUND); + + } +} diff --git a/src/main/java/com/moing/backend/domain/missionRead/application/mapper/MissionReadMapper.java b/src/main/java/com/moing/backend/domain/missionRead/application/mapper/MissionReadMapper.java new file mode 100644 index 00000000..3059d832 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionRead/application/mapper/MissionReadMapper.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.missionRead.application.mapper; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.missionRead.domain.entity.MissionRead; +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.stereotype.Component; + +@Component +public class MissionReadMapper { + public static MissionRead toMissionRead(Team team, Member member){ + MissionRead missionRead=new MissionRead(); + missionRead.updateTeam(team); + missionRead.updateMember(member); + return missionRead; + } +} diff --git a/src/main/java/com/moing/backend/domain/missionRead/application/service/CreateMissionReadUseCase.java b/src/main/java/com/moing/backend/domain/missionRead/application/service/CreateMissionReadUseCase.java new file mode 100644 index 00000000..1989d180 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionRead/application/service/CreateMissionReadUseCase.java @@ -0,0 +1,25 @@ +package com.moing.backend.domain.missionRead.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.missionRead.application.mapper.MissionReadMapper; +import com.moing.backend.domain.missionRead.domain.entity.MissionRead; +import com.moing.backend.domain.missionRead.domain.service.MissionReadSaveService; +import com.moing.backend.domain.team.domain.entity.Team; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class CreateMissionReadUseCase { + + private final MissionReadSaveService missionReadSaveService; + + public void createMissionRead(Team team, Member member, Mission mission) { + MissionRead missionRead = MissionReadMapper.toMissionRead(team, member); + missionReadSaveService.saveMissionRead(mission, missionRead); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionRead/domain/entity/MissionRead.java b/src/main/java/com/moing/backend/domain/missionRead/domain/entity/MissionRead.java new file mode 100644 index 00000000..f00939f5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionRead/domain/entity/MissionRead.java @@ -0,0 +1,54 @@ +package com.moing.backend.domain.missionRead.domain.entity; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class MissionRead extends BaseTimeEntity{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "mission_read_id") + private Long missionReadId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "mission_id") + private Mission mission; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team__id") + private Team team; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "Member_id") + private Member member; + + + /** + * 연관관계 매핑 + */ + public void updateMission(Mission mission) { + this.mission=mission; + } + + public void updateTeam(Team team) { + this.team = team; + } + + public void updateMember(Member member) { + this.member = member; + } +} diff --git a/src/main/java/com/moing/backend/domain/missionRead/domain/repository/MissionReadRepository.java b/src/main/java/com/moing/backend/domain/missionRead/domain/repository/MissionReadRepository.java new file mode 100644 index 00000000..64baf36f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionRead/domain/repository/MissionReadRepository.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.missionRead.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.missionRead.domain.entity.MissionRead; +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MissionReadRepository extends JpaRepository { + + List findMissionReadByMissionAndMemberAndTeam(Mission mission, Member member, Team team); +} diff --git a/src/main/java/com/moing/backend/domain/missionRead/domain/repository/MissionReadRepositoryUtils.java b/src/main/java/com/moing/backend/domain/missionRead/domain/repository/MissionReadRepositoryUtils.java new file mode 100644 index 00000000..d3d715ca --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionRead/domain/repository/MissionReadRepositoryUtils.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.missionRead.domain.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; + +import java.util.List; + +import static com.moing.backend.domain.mission.domain.entity.QMission.mission; +import static com.moing.backend.domain.missionRead.domain.entity.QMissionRead.missionRead; + +public class MissionReadRepositoryUtils { + public static BooleanExpression isMissionReadByMemberIdAndTeamId(Long memberId, Long teamId) { + return JPAExpressions + .select(missionRead.missionReadId) + .from(missionRead) + .where(missionRead.member.memberId.eq(memberId), + missionRead.mission.team.teamId.eq(teamId), + missionRead.mission.id.eq(mission.id)) + .exists(); + } + + public static BooleanExpression isMissionReadByMemberIdAndTeamIds(Long memberId, List teamIds){ + return JPAExpressions + .select(missionRead.missionReadId) + .from(missionRead) + .where(missionRead.member.memberId.eq(memberId), + missionRead.mission.team.teamId.in(teamIds), + missionRead.mission.id.eq(mission.id)) + .exists(); + } +} diff --git a/src/main/java/com/moing/backend/domain/missionRead/domain/service/MissionReadSaveService.java b/src/main/java/com/moing/backend/domain/missionRead/domain/service/MissionReadSaveService.java new file mode 100644 index 00000000..8dc37ac2 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/missionRead/domain/service/MissionReadSaveService.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.missionRead.domain.service; + +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.missionRead.domain.entity.MissionRead; +import com.moing.backend.domain.missionRead.domain.repository.MissionReadRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.List; + +@DomainService +@RequiredArgsConstructor +@Transactional +public class MissionReadSaveService { + + private final MissionReadRepository missionReadRepository; + + + public void saveMissionRead(Mission mission, MissionRead missionRead) { + List existingMissionReads = missionReadRepository.findMissionReadByMissionAndMemberAndTeam(mission, missionRead.getMember(), missionRead.getTeam()); + + if (existingMissionReads.isEmpty()) { + missionRead.updateMission(mission); + missionReadRepository.save(missionRead); + } + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/dto/request/UpdateProfileRequest.java b/src/main/java/com/moing/backend/domain/mypage/application/dto/request/UpdateProfileRequest.java new file mode 100644 index 00000000..578f59d0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/dto/request/UpdateProfileRequest.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.mypage.application.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class UpdateProfileRequest { + + private String profileImage; + private String nickName; + private String introduction; +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/dto/request/WithdrawRequest.java b/src/main/java/com/moing/backend/domain/mypage/application/dto/request/WithdrawRequest.java new file mode 100644 index 00000000..1b07d726 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/dto/request/WithdrawRequest.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.mypage.application.dto.request; + +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 WithdrawRequest { + @NotBlank(message = "reason 을 입력해주세요.") + @Size(min = 1, max = 500, message="reason 은 최소 1개, 최대 500개의 문자만 입력 가능합니다.") + private String reason; + + @NotBlank(message = "socialToken 을 입력해주세요.") + private String socialToken; +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetAlarmResponse.java b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetAlarmResponse.java new file mode 100644 index 00000000..9e858a34 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetAlarmResponse.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.mypage.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class GetAlarmResponse { + + private boolean isNewUploadPush; + private boolean isRemindPush; + private boolean isFirePush; + private boolean isCommentPush; +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetMyPageResponse.java b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetMyPageResponse.java new file mode 100644 index 00000000..f4193f11 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetMyPageResponse.java @@ -0,0 +1,21 @@ +package com.moing.backend.domain.mypage.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class GetMyPageResponse { + private String profileImage; + private String nickName; + private String introduction; + private List categories=new ArrayList<>(); + private List getMyPageTeamBlocks = new ArrayList<>(); +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetMyPageTeamBlock.java b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetMyPageTeamBlock.java new file mode 100644 index 00000000..ba86cc7a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetMyPageTeamBlock.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.mypage.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class GetMyPageTeamBlock { + private Long teamId; + private String teamName; + private String category; + private String profileImgUrl; +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetProfileResponse.java b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetProfileResponse.java new file mode 100644 index 00000000..5b6d2e79 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/dto/response/GetProfileResponse.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.mypage.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class GetProfileResponse { + private String profileImage; + private String nickName; + private String introduction; +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/mapper/MyPageMapper.java b/src/main/java/com/moing/backend/domain/mypage/application/mapper/MyPageMapper.java new file mode 100644 index 00000000..e638f2f3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/mapper/MyPageMapper.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.mypage.application.mapper; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageResponse; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageTeamBlock; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class MyPageMapper { + + public static GetMyPageResponse toGetMyPageResponse(Member member, List categories, List blocks) { + return GetMyPageResponse.builder() + .profileImage(member.getProfileImage()) + .nickName(member.getNickName()) + .introduction(member.getIntroduction()) + .categories(categories) + .getMyPageTeamBlocks(blocks) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/service/AlarmUseCase.java b/src/main/java/com/moing/backend/domain/mypage/application/service/AlarmUseCase.java new file mode 100644 index 00000000..5d7c7e78 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/service/AlarmUseCase.java @@ -0,0 +1,48 @@ +package com.moing.backend.domain.mypage.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mypage.application.dto.response.GetAlarmResponse; +import com.moing.backend.domain.mypage.exception.AlarmInvalidException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AlarmUseCase { + + private final MemberGetService memberGetService; + + @Transactional(readOnly = true) + public GetAlarmResponse getAlarm(String socialId){ + Member member=memberGetService.getMemberBySocialId(socialId); + return new GetAlarmResponse(member.isNewUploadPush(),member.isRemindPush(), member.isFirePush(), member.isCommentPush()); + } + + @Transactional + public GetAlarmResponse updateAlarm(String socialId, String type, String status) { + Member member = memberGetService.getMemberBySocialId(socialId); + boolean push = "on".equals(status); + + switch (type) { + case "all": + member.updateAllPush(push); + break; + case "isNewUploadPush": + member.updateNewUploadPush(push); + break; + case "isRemindPush": + member.updateRemindPush(push); + break; + case "isFirePush": + member.updateFirePush(push); + break; + case "isCommentPush": + member.updateCommentPush(push); + default: + throw new AlarmInvalidException(); + } + return new GetAlarmResponse(member.isNewUploadPush(),member.isRemindPush(), member.isFirePush(), member.isCommentPush()); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/service/GetMyPageUseCase.java b/src/main/java/com/moing/backend/domain/mypage/application/service/GetMyPageUseCase.java new file mode 100644 index 00000000..51d80c77 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/service/GetMyPageUseCase.java @@ -0,0 +1,38 @@ +package com.moing.backend.domain.mypage.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageResponse; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageTeamBlock; +import com.moing.backend.domain.mypage.application.mapper.MyPageMapper; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class GetMyPageUseCase { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + + @Transactional(readOnly = true) + public GetMyPageResponse getMyPageResponse(String socialId) { + Member member = memberGetService.getMemberBySocialId(socialId); + List getMyPageTeamBlocks = teamGetService.getMyPageTeamBlockByMemberId(member.getMemberId()); + return MyPageMapper.toGetMyPageResponse(member, calculateCategory(getMyPageTeamBlocks), getMyPageTeamBlocks); + } + + private static List calculateCategory(List getMyPageTeamBlocks) { + return getMyPageTeamBlocks.stream() + .map(GetMyPageTeamBlock::getCategory) + .distinct() + .limit(2) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/service/ProfileUseCase.java b/src/main/java/com/moing/backend/domain/mypage/application/service/ProfileUseCase.java new file mode 100644 index 00000000..f498edaa --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/service/ProfileUseCase.java @@ -0,0 +1,45 @@ +package com.moing.backend.domain.mypage.application.service; + +import com.moing.backend.domain.auth.exception.NicknameDuplicationException; +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.domain.mypage.application.dto.request.UpdateProfileRequest; +import com.moing.backend.domain.mypage.application.dto.response.GetProfileResponse; +import com.moing.backend.global.utils.UpdateUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProfileUseCase { + + private final MemberGetService memberGetService; + private final UpdateUtils updateUtils; + private final MemberCheckService memberCheckService; + + @Transactional(readOnly = true) + public GetProfileResponse getProfile(String socialId){ + Member member=memberGetService.getMemberBySocialId(socialId); + return new GetProfileResponse(member.getProfileImage(), member.getNickName(), member.getIntroduction()); + } + + @Transactional + public void updateProfile(String socialId, UpdateProfileRequest updateProfileRequest) { + Member member = memberGetService.getMemberBySocialId(socialId); + String oldProfileImageUrl = member.getProfileImage(); + if(updateProfileRequest.getNickName()!=null){ + if(memberCheckService.checkNickname(updateProfileRequest.getNickName())) throw new NicknameDuplicationException(); //닉네임 중복검사 (이중체크) + } + + member.updateProfile( + UpdateUtils.getUpdatedValue(updateProfileRequest.getProfileImage(), member.getProfileImage()), + UpdateUtils.getUpdatedValue(updateProfileRequest.getNickName(), member.getNickName()), + UpdateUtils.getUpdatedValue(updateProfileRequest.getIntroduction(), member.getIntroduction()) + ); + + updateUtils.deleteOldImgUrl(updateProfileRequest.getProfileImage(), oldProfileImageUrl); + } + +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/service/SignOutUseCase.java b/src/main/java/com/moing/backend/domain/mypage/application/service/SignOutUseCase.java new file mode 100644 index 00000000..af504fd4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/service/SignOutUseCase.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.mypage.application.service; + +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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class SignOutUseCase { + + private final TokenUtil tokenUtil; + private final MemberGetService memberGetService; + + public void signOut(String socialId){ + tokenUtil.expireRefreshToken(socialId); + Member member=memberGetService.getMemberBySocialId(socialId); + member.signOut(); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/application/service/WithdrawUseCase.java b/src/main/java/com/moing/backend/domain/mypage/application/service/WithdrawUseCase.java new file mode 100644 index 00000000..bfda5361 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/application/service/WithdrawUseCase.java @@ -0,0 +1,52 @@ +package com.moing.backend.domain.mypage.application.service; + +import com.moing.backend.domain.auth.application.service.WithdrawProvider; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mypage.application.dto.request.WithdrawRequest; +import com.moing.backend.domain.mypage.domain.service.FeedbackSaveService; +import com.moing.backend.domain.mypage.exception.ExistingTeamException; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.global.config.security.jwt.TokenUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.io.IOException; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class WithdrawUseCase { + + private final MemberGetService memberGetService; + private final FeedbackSaveService feedbackSaveService; + private final TokenUtil tokenUtil; + private final TeamGetService teamGetService; + private final Map withdrawProviders; + + @Transactional + public void withdraw(String socialId, String providerInfo, WithdrawRequest withdrawRequest) throws IOException { + Member member = memberGetService.getMemberBySocialId(socialId); + checkMemberIsNotPartOfAnyTeam(member); + socialWithdraw(providerInfo, withdrawRequest.getSocialToken()); + member.deleteMember(); + feedbackSaveService.saveFeedback(member, withdrawRequest); + tokenUtil.expireRefreshToken(socialId); + } + + private void socialWithdraw(String providerInfo, String token) throws IOException { + WithdrawProvider withdrawProvider=withdrawProviders.get(providerInfo+"Withdraw"); + if (withdrawProvider == null) { + throw new IllegalArgumentException("Unknown provider: " + providerInfo); + } + withdrawProvider.withdraw(token); + } + + private void checkMemberIsNotPartOfAnyTeam(Member member) { + if (!teamGetService.getTeamIdByMemberId(member.getMemberId()).isEmpty()) { + throw new ExistingTeamException(); + } + } +} + diff --git a/src/main/java/com/moing/backend/domain/mypage/domain/entity/Feedback.java b/src/main/java/com/moing/backend/domain/mypage/domain/entity/Feedback.java new file mode 100644 index 00000000..c6ed237b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/domain/entity/Feedback.java @@ -0,0 +1,32 @@ +package com.moing.backend.domain.mypage.domain.entity; + +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; + + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Entity +public class Feedback extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedback_id") + private Long feedbackId; + + @Column(nullable = false) + private Long memberId; + + @Column(nullable = false, length = 500) + private String reason; + + public Feedback(Long memberId, String reason){ + this.memberId=memberId; + this.reason=reason; + } + +} diff --git a/src/main/java/com/moing/backend/domain/mypage/domain/repository/FeedbackRepository.java b/src/main/java/com/moing/backend/domain/mypage/domain/repository/FeedbackRepository.java new file mode 100644 index 00000000..857fb7b2 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/domain/repository/FeedbackRepository.java @@ -0,0 +1,7 @@ +package com.moing.backend.domain.mypage.domain.repository; + +import com.moing.backend.domain.mypage.domain.entity.Feedback; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FeedbackRepository extends JpaRepository { +} diff --git a/src/main/java/com/moing/backend/domain/mypage/domain/service/FeedbackSaveService.java b/src/main/java/com/moing/backend/domain/mypage/domain/service/FeedbackSaveService.java new file mode 100644 index 00000000..163b692c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/domain/service/FeedbackSaveService.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.mypage.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.mypage.application.dto.request.WithdrawRequest; +import com.moing.backend.domain.mypage.domain.entity.Feedback; +import com.moing.backend.domain.mypage.domain.repository.FeedbackRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; + +@DomainService +@RequiredArgsConstructor +public class FeedbackSaveService { + private final FeedbackRepository feedbackRepository; + + @Transactional + public void saveFeedback(Member member, WithdrawRequest withdrawRequest){ + Feedback feedback=new Feedback(member.getMemberId(), withdrawRequest.getReason()); + feedbackRepository.save(feedback); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/exception/AlarmInvalidException.java b/src/main/java/com/moing/backend/domain/mypage/exception/AlarmInvalidException.java new file mode 100644 index 00000000..6fd3c4d2 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/exception/AlarmInvalidException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.mypage.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class AlarmInvalidException extends MyPageException { + public AlarmInvalidException() { + super(ErrorCode.INVALID_ALARM_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/exception/ExistingTeamException.java b/src/main/java/com/moing/backend/domain/mypage/exception/ExistingTeamException.java new file mode 100644 index 00000000..e5f87913 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/exception/ExistingTeamException.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.mypage.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class ExistingTeamException extends ApplicationException { + public ExistingTeamException() { + super(ErrorCode.EXISTING_TEAM_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/exception/MyPageException.java b/src/main/java/com/moing/backend/domain/mypage/exception/MyPageException.java new file mode 100644 index 00000000..20aabdc1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/exception/MyPageException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.mypage.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class MyPageException extends ApplicationException { + protected MyPageException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/domain/mypage/presentation/MyPageController.java b/src/main/java/com/moing/backend/domain/mypage/presentation/MyPageController.java new file mode 100644 index 00000000..f03563aa --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/presentation/MyPageController.java @@ -0,0 +1,119 @@ +package com.moing.backend.domain.mypage.presentation; + +import com.moing.backend.domain.mypage.application.dto.request.UpdateProfileRequest; +import com.moing.backend.domain.mypage.application.dto.request.WithdrawRequest; +import com.moing.backend.domain.mypage.application.dto.response.GetAlarmResponse; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageResponse; +import com.moing.backend.domain.mypage.application.dto.response.GetProfileResponse; +import com.moing.backend.domain.mypage.application.service.*; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +import java.io.IOException; + +import static com.moing.backend.domain.mypage.presentation.constant.MypageResponseMessage.*; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/mypage") +public class MyPageController { + + private final SignOutUseCase signOutService; + private final WithdrawUseCase withdrawService; + private final ProfileUseCase profileUseCase; + private final AlarmUseCase alarmUseCase; + private final GetMyPageUseCase getMyPageUseCase; + + /** + * 사용자 회원가입 여부 + * [GET] api/mypage/test + * 작성자: 김민수 + */ + @GetMapping("/test") + public ResponseEntity test(@AuthenticationPrincipal User user){ + return ResponseEntity.ok(SuccessResponse.create(TEST_SIGNUP_SUCCESS.getMessage())); + } + /** + * 로그아웃 + * [POST] api/mypage/signOut + * 작성자 : 김민수 + */ + @PostMapping("/signOut") + public ResponseEntity signOut(@AuthenticationPrincipal User user) { + this.signOutService.signOut(user.getSocialId()); + return ResponseEntity.ok(SuccessResponse.create(SIGN_OUT_SUCCESS.getMessage())); + } + + /** + * 회원탈퇴 + * [DELETE] api/mypage/withdrawal + * 작성자 : 김민수 + */ + @DeleteMapping("/withdrawal/{provider}") + public ResponseEntity withdraw(@AuthenticationPrincipal User user, + @PathVariable String provider, + @Valid @RequestBody WithdrawRequest withdrawRequest) throws IOException { + this.withdrawService.withdraw(user.getSocialId(), provider, withdrawRequest); + return ResponseEntity.ok(SuccessResponse.create(WITHDRAWAL_SUCCESS.getMessage())); + } + + /** + * 마이페이지 조회 + * [GET] api/mypage + * 작성자: 김민수 + */ + @GetMapping + public ResponseEntity> getMyPage(@AuthenticationPrincipal User user){ + return ResponseEntity.ok(SuccessResponse.create(GET_MYPAGE_SUCCESS.getMessage(), this.getMyPageUseCase.getMyPageResponse(user.getSocialId()))); + } + + /** + * 프로필 조회 + * [GET] api/mypage/profile + * 작성자 : 김민수 + */ + @GetMapping("/profile") + public ResponseEntity> getProfile(@AuthenticationPrincipal User user){ + return ResponseEntity.ok(SuccessResponse.create(GET_PROFILE_SUCCESS.getMessage(), this.profileUseCase.getProfile(user.getSocialId()))); + } + + /** + * 프로필 수정 + * [PUT] api/mypage/profile + * 작성자 : 김민수 + */ + @PutMapping("/profile") + public ResponseEntity updatePorfile(@AuthenticationPrincipal User user, + @RequestBody UpdateProfileRequest updateProfileRequest){ + this.profileUseCase.updateProfile(user.getSocialId(), updateProfileRequest); + return ResponseEntity.ok(SuccessResponse.create(UPDATE_PROFILE_SUCCESS.getMessage())); + } + + /** + * 알림정보 조회 + * [GET] api/mypage/alarm + * 작성자 : 김민수 + */ + @GetMapping("/alarm") + public ResponseEntity> getAlarm(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_ALARM_SUCCESS.getMessage(), this.alarmUseCase.getAlarm(user.getSocialId()))); + } + + /** + * 알림정보 수정 + * [POST] api/mypage/alarm?type=all || isNewUploadPush || isRemindPush || isFirePush || isCommentPush && status= on || off + */ + @PutMapping("/alarm") + public ResponseEntity> updateAlarm(@AuthenticationPrincipal User user, + @RequestParam(name = "type") String type, + @RequestParam(name = "status") String status) { + return ResponseEntity.ok(SuccessResponse.create(UPDATE_PROFILE_SUCCESS.getMessage(), this.alarmUseCase.updateAlarm(user.getSocialId(), type, status))); + } + +} diff --git a/src/main/java/com/moing/backend/domain/mypage/presentation/constant/MypageResponseMessage.java b/src/main/java/com/moing/backend/domain/mypage/presentation/constant/MypageResponseMessage.java new file mode 100644 index 00000000..d7aa915d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/mypage/presentation/constant/MypageResponseMessage.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.mypage.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MypageResponseMessage { + TEST_SIGNUP_SUCCESS("회원가입이 되었습니다."), + SIGN_OUT_SUCCESS("로그아웃을 했습니다"), + WITHDRAWAL_SUCCESS("회원탈퇴를 했습니다"), + GET_MYPAGE_SUCCESS("마이페이지를 조회했습니다"), + GET_PROFILE_SUCCESS("프로필을 조회했습니다"), + UPDATE_PROFILE_SUCCESS("프로필을 수정했습니다"), + GET_ALARM_SUCCESS("알람 정보를 조회했습니다"), + UPDATE_ALARM_SUCCESS("알람 정보를 수정했습니다"); + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/report/application/dto/BlockMemberRes.java b/src/main/java/com/moing/backend/domain/report/application/dto/BlockMemberRes.java new file mode 100644 index 00000000..1c7563d0 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/dto/BlockMemberRes.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.report.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +@AllArgsConstructor +public class BlockMemberRes { + private Long targetId; + private String nickName; + private String introduce; + private String profileImg; +} diff --git a/src/main/java/com/moing/backend/domain/report/application/mapper/ReportMapper.java b/src/main/java/com/moing/backend/domain/report/application/mapper/ReportMapper.java new file mode 100644 index 00000000..2d2d4472 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/mapper/ReportMapper.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.report.application.mapper; + +import com.moing.backend.domain.report.domain.entity.Report; +import com.moing.backend.domain.report.domain.entity.constant.ReportType; +import com.moing.backend.global.annotation.Mapper; + +@Mapper +public class ReportMapper { + + public static Report mapToReport(Long memberId, Long targetId, String reportType,String targetMemberNickName) { + return Report.builder() + .reportMemberId(memberId) + .reportType(ReportType.valueOf(reportType)) + .targetId(targetId) + .targetMemberNickName(targetMemberNickName) + .build(); + } + +} diff --git a/src/main/java/com/moing/backend/domain/report/application/service/BoardCommentReportStrategy.java b/src/main/java/com/moing/backend/domain/report/application/service/BoardCommentReportStrategy.java new file mode 100644 index 00000000..d3be8ffa --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/service/BoardCommentReportStrategy.java @@ -0,0 +1,30 @@ +package com.moing.backend.domain.report.application.service; + +import com.moing.backend.domain.boardComment.domain.entity.BoardComment; +import com.moing.backend.domain.boardComment.domain.service.BoardCommentGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.REPORT_MESSAGE; + +@Service +@Transactional +@RequiredArgsConstructor +public class BoardCommentReportStrategy implements ReportStrategy { + + + private final BoardCommentGetService boardCommentGetService; + + + @Override + public String processReport(Long targetId) { + BoardComment boardComment = boardCommentGetService.getComment(targetId); + boardComment.updateContent(REPORT_MESSAGE.getMessage()); + return getTargetMemberNickName(boardComment); + } + + private String getTargetMemberNickName(BoardComment boardComment){ + return boardComment.getWriterNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/report/application/service/BoardReportStrategy.java b/src/main/java/com/moing/backend/domain/report/application/service/BoardReportStrategy.java new file mode 100644 index 00000000..a5cf41c1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/service/BoardReportStrategy.java @@ -0,0 +1,33 @@ +package com.moing.backend.domain.report.application.service; + +import com.moing.backend.domain.board.application.dto.request.UpdateBoardRequest; +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.board.domain.service.BoardGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.REPORT_MESSAGE; + +@Service +@Transactional +@RequiredArgsConstructor +public class BoardReportStrategy implements ReportStrategy { + + private final BoardGetService boardGetService; + + @Override + public String processReport(Long targetId) { + Board board = boardGetService.getBoard(targetId); + board.updateBoard(UpdateBoardRequest.builder() + .title(REPORT_MESSAGE.getMessage()) + .content(REPORT_MESSAGE.getMessage()) + .isNotice(board.isNotice()) + .build()); + return getTargetMemberNickName(board); + } + + private String getTargetMemberNickName(Board board){ + return board.getWriterNickName(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/report/application/service/MissionArchiveReportStrategy.java b/src/main/java/com/moing/backend/domain/report/application/service/MissionArchiveReportStrategy.java new file mode 100644 index 00000000..c2485915 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/service/MissionArchiveReportStrategy.java @@ -0,0 +1,45 @@ +package com.moing.backend.domain.report.application.service; + +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionWay; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchiveStatus; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.REPORT_MESSAGE; +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.REPORT_PHOTO; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionArchiveReportStrategy implements ReportStrategy { + + + private final MissionArchiveQueryService missionArchiveQueryService; + + + @Override + public String processReport(Long targetId) { + MissionArchive missionArchive = missionArchiveQueryService.findByMissionArchiveId(targetId); + + if (isCompletedPhotoArchive(missionArchive)) { + missionArchive.updateArchive(REPORT_PHOTO.getMessage()); + } else { + missionArchive.updateArchive(REPORT_MESSAGE.getMessage()); + } + + return getTargetMemberNickName(missionArchive); + } + + private String getTargetMemberNickName(MissionArchive missionArchive){ + return missionArchive.getWriterNickName(); + } + + private Boolean isCompletedPhotoArchive (MissionArchive archive) { + return archive.getMission().getWay().equals(MissionWay.PHOTO) && archive.getStatus().equals(MissionArchiveStatus.COMPLETE); + } +} diff --git a/src/main/java/com/moing/backend/domain/report/application/service/MissionCommentReportStrategy.java b/src/main/java/com/moing/backend/domain/report/application/service/MissionCommentReportStrategy.java new file mode 100644 index 00000000..b9eab647 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/service/MissionCommentReportStrategy.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.report.application.service; + +import com.moing.backend.domain.missionComment.domain.entity.MissionComment; +import com.moing.backend.domain.missionComment.domain.service.MissionCommentGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.REPORT_MESSAGE; + +@Service +@Transactional +@RequiredArgsConstructor +public class MissionCommentReportStrategy implements ReportStrategy { + + + private final MissionCommentGetService missionCommentGetService; + + + @Override + public String processReport(Long targetId) { + MissionComment missionComment=missionCommentGetService.getComment(targetId); + missionComment.updateContent(REPORT_MESSAGE.getMessage()); + + return getTargetMemberNickName(missionComment); + } + + private String getTargetMemberNickName(MissionComment missionComment){ + return missionComment.getWriterNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/report/application/service/ReportCreateUseCase.java b/src/main/java/com/moing/backend/domain/report/application/service/ReportCreateUseCase.java new file mode 100644 index 00000000..e3805608 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/service/ReportCreateUseCase.java @@ -0,0 +1,30 @@ +package com.moing.backend.domain.report.application.service; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.report.application.mapper.ReportMapper; +import com.moing.backend.domain.report.domain.entity.Report; +import com.moing.backend.domain.report.domain.service.ReportSaveService; +import com.moing.backend.domain.report.presentation.constant.StrategyCategory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReportCreateUseCase { + + private final ReportSaveService reportSaveService; + private final Map strategyMap; + private final MemberGetService memberGetService; + + public Long createReport(String socialId, Long targetId, String reportType) { + ReportStrategy strategy = strategyMap.get(StrategyCategory.valueOf(reportType).getStrategyName()); + Long memberId = memberGetService.getMemberBySocialId(socialId).getMemberId(); + String targetMemberNickName= strategy.processReport(targetId); + Report save = reportSaveService.save(ReportMapper.mapToReport(memberId, targetId, reportType, targetMemberNickName)); + return save.getTargetId(); + } +} diff --git a/src/main/java/com/moing/backend/domain/report/application/service/ReportStrategy.java b/src/main/java/com/moing/backend/domain/report/application/service/ReportStrategy.java new file mode 100644 index 00000000..45f1f2d3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/application/service/ReportStrategy.java @@ -0,0 +1,6 @@ +package com.moing.backend.domain.report.application.service; + +public interface ReportStrategy { + String processReport(Long targetId); + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/report/domain/entity/Report.java b/src/main/java/com/moing/backend/domain/report/domain/entity/Report.java new file mode 100644 index 00000000..36146585 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/domain/entity/Report.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.report.domain.entity; + + +import com.moing.backend.domain.report.domain.entity.constant.ReportType; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Builder +public class Report extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "report_id") + private Long id; + + @Enumerated(EnumType.STRING) + private ReportType reportType; + + private Long reportMemberId; + private Long targetId; + private String targetMemberNickName; +} diff --git a/src/main/java/com/moing/backend/domain/report/domain/entity/constant/ReportType.java b/src/main/java/com/moing/backend/domain/report/domain/entity/constant/ReportType.java new file mode 100644 index 00000000..2dfe63d3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/domain/entity/constant/ReportType.java @@ -0,0 +1,5 @@ +package com.moing.backend.domain.report.domain.entity.constant; + +public enum ReportType { + MISSION, BOARD, BCOMMENT, MCOMMENT +} diff --git a/src/main/java/com/moing/backend/domain/report/domain/repository/ReportRepository.java b/src/main/java/com/moing/backend/domain/report/domain/repository/ReportRepository.java new file mode 100644 index 00000000..50c5b296 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/domain/repository/ReportRepository.java @@ -0,0 +1,8 @@ +package com.moing.backend.domain.report.domain.repository; + +import com.moing.backend.domain.report.domain.entity.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moing/backend/domain/report/domain/service/ReportSaveService.java b/src/main/java/com/moing/backend/domain/report/domain/service/ReportSaveService.java new file mode 100644 index 00000000..00c3fce6 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/domain/service/ReportSaveService.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.report.domain.service; + +import com.moing.backend.domain.report.domain.entity.Report; +import com.moing.backend.domain.report.domain.repository.ReportRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class ReportSaveService { + + private final ReportRepository reportRepository; + + public Report save(Report report) { + return reportRepository.save(report); + } +} diff --git a/src/main/java/com/moing/backend/domain/report/presentation/ReportController.java b/src/main/java/com/moing/backend/domain/report/presentation/ReportController.java new file mode 100644 index 00000000..5489aedb --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/presentation/ReportController.java @@ -0,0 +1,29 @@ +package com.moing.backend.domain.report.presentation; + +import com.moing.backend.domain.report.application.service.ReportCreateUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.CREATE_REPORT_SUCCESS; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/report") +public class ReportController { + + private final ReportCreateUseCase reportCreateUseCase; + + @PostMapping("/{reportType}/{targetId}") + public ResponseEntity> createReport(@AuthenticationPrincipal User user, + @PathVariable("reportType") String reportType, + @PathVariable("targetId") Long targetId) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_REPORT_SUCCESS.getMessage(), this.reportCreateUseCase.createReport(user.getSocialId(), targetId,reportType))); + } +} diff --git a/src/main/java/com/moing/backend/domain/report/presentation/constant/ReportResponseMessage.java b/src/main/java/com/moing/backend/domain/report/presentation/constant/ReportResponseMessage.java new file mode 100644 index 00000000..a7332498 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/presentation/constant/ReportResponseMessage.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.report.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReportResponseMessage { + + CREATE_REPORT_SUCCESS("게시글 신고를 완료 했습니다."), + REPORT_MESSAGE("신고 접수로 삭제되었습니다."), + REPORT_PHOTO("https://mo-ing.s3.ap-northeast-2.amazonaws.com/reportImage.png"); + + private final String message; + + +} + diff --git a/src/main/java/com/moing/backend/domain/report/presentation/constant/StrategyCategory.java b/src/main/java/com/moing/backend/domain/report/presentation/constant/StrategyCategory.java new file mode 100644 index 00000000..b2acdf43 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/report/presentation/constant/StrategyCategory.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.report.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum StrategyCategory { + + BCOMMENT("boardCommentReportStrategy"), + BOARD("boardReportStrategy"), + MISSION("missionArchiveReportStrategy"), + MCOMMENT("missionCommentReportStrategy"); + + private final String strategyName; +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java b/src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java new file mode 100644 index 00000000..8c434da9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/dto/DailyStats.java @@ -0,0 +1,24 @@ +package com.moing.backend.domain.statistics.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.SqlResultSetMapping; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class DailyStats { + + private long todayNewMembers; + private long yesterdayNewMembers; + private long todayNewTeams; + private long yesterdayNewTeams; + private long todayRepeatMission; + private long yesterdayRepeatMission; + private long todayOnceMission; + private long yesterdayOnceMission; + +} + diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUFire.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUFire.java new file mode 100644 index 00000000..526e1366 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUFire.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.fire.domain.service.FireQueryService; +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DAUFire implements DAUProvider { + + private final FireQueryService fireQueryService; + + @Override + public Long getTodayStats() { + return fireQueryService.getTodayFires(); + } + + @Override + public Long getYesterdayStats() { + return fireQueryService.getYesterdayFires(); + } + + @Override + public DAUStatusType getSupportedType() { + return DAUStatusType.DAILY_FIRE_COUNT; + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUManager.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUManager.java new file mode 100644 index 00000000..c40b7655 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUManager.java @@ -0,0 +1,38 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +@Component +public class DAUManager { + + private final Map serviceMap = new EnumMap<>(DAUStatusType.class); + + @Autowired + public DAUManager(List services) { + for (DAUProvider service : services) { + serviceMap.put(service.getSupportedType(), service); + } + } + + public Long getTodayStats(DAUStatusType type) { + DAUProvider service = serviceMap.get(type); + if (service != null) { + return service.getTodayStats(); + } + throw new IllegalArgumentException("Unsupported DAUStatusType: " + type); + } + + public Long getYesterdayStats(DAUStatusType type) { + DAUProvider service = serviceMap.get(type); + if (service != null) { + return service.getYesterdayStats(); + } + throw new IllegalArgumentException("Unsupported DAUStatusType: " + type); + } +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUMember.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUMember.java new file mode 100644 index 00000000..d0908092 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUMember.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DAUMember implements DAUProvider { + + private final MemberGetService memberGetService; + + @Override + public Long getTodayStats() { + return memberGetService.getTodayNewMembers(); + } + + @Override + public Long getYesterdayStats() { + return memberGetService.getYesterdayNewMembers(); + } + + @Override + public DAUStatusType getSupportedType() { + return DAUStatusType.DAILY_MEMBER_COUNT; + } +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUMissionArchive.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUMissionArchive.java new file mode 100644 index 00000000..b2ce9698 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUMissionArchive.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DAUMissionArchive implements DAUProvider { + + private final MissionArchiveQueryService missionArchiveQueryService; + + @Override + public Long getTodayStats() { + return missionArchiveQueryService.getTodayMissionArchives(); + } + + @Override + public Long getYesterdayStats() { + return missionArchiveQueryService.getYesterdayMissionArchives(); + } + + @Override + public DAUStatusType getSupportedType() { + return DAUStatusType.DAILY_MISSION_ARCHIVE_COUNT; + } +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUOnceMission.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUOnceMission.java new file mode 100644 index 00000000..ac8bb719 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUOnceMission.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DAUOnceMission implements DAUProvider { + private final MissionQueryService missionQueryService; + + @Override + public Long getTodayStats() { + return missionQueryService.getTodayOnceMissions(); + } + + @Override + public Long getYesterdayStats() { + return missionQueryService.getYesterdayOnceMissions(); + } + + @Override + public DAUStatusType getSupportedType() { + return DAUStatusType.DAILY_ONCE_MISSION_COUNT; + } +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUProvider.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUProvider.java new file mode 100644 index 00000000..9af66f78 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUProvider.java @@ -0,0 +1,9 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; + +public interface DAUProvider { + Long getTodayStats(); + Long getYesterdayStats(); + DAUStatusType getSupportedType(); +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAURepeatMission.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAURepeatMission.java new file mode 100644 index 00000000..cc48fd79 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAURepeatMission.java @@ -0,0 +1,27 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DAURepeatMission implements DAUProvider { + private final MissionQueryService missionQueryService; + + @Override + public Long getTodayStats() { + return missionQueryService.getTodayRepeatMissions(); + } + + @Override + public Long getYesterdayStats() { + return missionQueryService.getYesterdayRepeatMissions(); + } + + @Override + public DAUStatusType getSupportedType() { + return DAUStatusType.DAILY_REPEAT_MISSION_COUNT; + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java new file mode 100644 index 00000000..a79dd59b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUScheduleUseCase.java @@ -0,0 +1,48 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import com.moing.backend.global.config.slack.util.WebhookUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Slf4j +@Service +@Transactional +@EnableAsync +@EnableScheduling +@RequiredArgsConstructor +@Profile("prod") +public class DAUScheduleUseCase { + + private final WebhookUtil webhookUtil; + private final DAUManager dauManager; + + /* + DAU 정보 : 일일 모임 생성 수, 일일 신규 가입자 수, 일일 반복 미션 생성 수, 일일 한번 미션 생성 수, 일일 미션 인증 수, 일일 불 던지기 생성 + */ + @Scheduled(cron = "0 55 23 * * *") + public void DailyInfoAlarm() { + Map todayStats = new LinkedHashMap<>(); + Map yesterdayStats = new LinkedHashMap<>(); + + + for (DAUStatusType type : DAUStatusType.values()) { + + todayStats.put(type.getMessage(), dauManager.getTodayStats(type)); + yesterdayStats.put(type.getMessage(), dauManager.getYesterdayStats(type)); + + } + + webhookUtil.sendDailyStatsMessage(todayStats, yesterdayStats); + } + +} diff --git a/src/main/java/com/moing/backend/domain/statistics/application/service/DAUTeam.java b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUTeam.java new file mode 100644 index 00000000..5eb38f5f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/application/service/DAUTeam.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.statistics.application.service; + +import com.moing.backend.domain.statistics.domain.constant.DAUStatusType; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class DAUTeam implements DAUProvider { + + private final TeamGetService teamGetService; + + @Override + public Long getTodayStats() { + return teamGetService.getTodayNewTeams(); + } + + @Override + public Long getYesterdayStats() { + return teamGetService.getYesterdayNewTeams(); + } + + @Override + public DAUStatusType getSupportedType() { + return DAUStatusType.DAILY_TEAM_COUNT; + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/statistics/domain/constant/DAUStatusType.java b/src/main/java/com/moing/backend/domain/statistics/domain/constant/DAUStatusType.java new file mode 100644 index 00000000..939304e1 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/statistics/domain/constant/DAUStatusType.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.statistics.domain.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum DAUStatusType { + DAILY_TEAM_COUNT("[DAU] 일일 모임 생성 수"), + DAILY_MEMBER_COUNT("[DAU] 일일 신규 가입자 수"), + DAILY_REPEAT_MISSION_COUNT("[DAU] 일일 반복 미션 생성 개수"), + DAILY_ONCE_MISSION_COUNT("[DAU] 일일 한번 미션 생성 개수"), + DAILY_MISSION_ARCHIVE_COUNT("[DAU] 일일 미션 인증 개수"), + DAILY_FIRE_COUNT("[DAU] 일일 불 던지기 생성 개수"); + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/request/CreateTeamRequest.java b/src/main/java/com/moing/backend/domain/team/application/dto/request/CreateTeamRequest.java new file mode 100644 index 00000000..9d4734f9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/request/CreateTeamRequest.java @@ -0,0 +1,34 @@ +package com.moing.backend.domain.team.application.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class CreateTeamRequest { + @NotBlank(message = "category 를 입력해 주세요.") + private String category; + + @NotBlank(message = "name 을 입력해 주세요.") + @Size(min = 1, max = 10, message = "name 은 최소 1개, 최대 10개의 문자만 입력 가능합니다.") + private String name; + + @NotBlank(message = "introduction 을 입력해 주세요.") + @Size(min = 1, max = 300, message = "introduction 은 최소 1개, 최대 300개의 문자만 입력 가능합니다.") + private String introduction; + + @NotBlank(message = "promise 를 입력해 주세요.") + @Size(min = 1, max = 100, message = "promise 는 최소 1개, 최대 100개의 문자만 입력 가능합니다.") + private String promise; + + @NotBlank(message = "profileImgUrl 을 입력해 주세요.") + private String profileImgUrl; + +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/request/UpdateTeamRequest.java b/src/main/java/com/moing/backend/domain/team/application/dto/request/UpdateTeamRequest.java new file mode 100644 index 00000000..d5863c72 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/request/UpdateTeamRequest.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.team.application.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Getter +public class UpdateTeamRequest { + + private String name; + + private String introduction; + + private String profileImgUrl; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/CreateTeamResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/CreateTeamResponse.java new file mode 100644 index 00000000..b87b04ce --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/CreateTeamResponse.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CreateTeamResponse { + private Long teamId; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/DeleteTeamResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/DeleteTeamResponse.java new file mode 100644 index 00000000..92ed9017 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/DeleteTeamResponse.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DeleteTeamResponse { + private Long teamId; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/GetCurrentStatusResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetCurrentStatusResponse.java new file mode 100644 index 00000000..32fa1745 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetCurrentStatusResponse.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class GetCurrentStatusResponse { + + private String name; + + private String introduction; + + private String profileImgUrl; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/GetLeaderInfoResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetLeaderInfoResponse.java new file mode 100644 index 00000000..0b04008d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetLeaderInfoResponse.java @@ -0,0 +1,18 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class GetLeaderInfoResponse { + + private Long teamId; + private String teamName; + private Long leaderId; + private String leaderName; + private String leaderFcmToken; + +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/GetNewTeamResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetNewTeamResponse.java new file mode 100644 index 00000000..6a741135 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetNewTeamResponse.java @@ -0,0 +1,21 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +@AllArgsConstructor +public class GetNewTeamResponse { + + private String teamName; + private String category; + private String promise; + private String introduction; + private String profileImgUrl; + private LocalDateTime createdDate; + +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamCountResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamCountResponse.java new file mode 100644 index 00000000..b4bd207a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamCountResponse.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.team.application.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class GetTeamCountResponse { + + private String teamName; + private Long numOfTeam; + private String leaderName; + private String memberName; + + public void updateCount(Long count){ + this.numOfTeam=count; + } + + public void updateMemberName(String nickName){ + this.memberName=nickName; + } + + @QueryProjection + public GetTeamCountResponse(String teamName, String leaderName){ + this.teamName=teamName; + this.leaderName=leaderName; + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamDetailResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamDetailResponse.java new file mode 100644 index 00000000..eb671bb4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamDetailResponse.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class GetTeamDetailResponse { + private Integer boardNum; //안 읽은 게시글 + private TeamInfo teamInfo; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamResponse.java new file mode 100644 index 00000000..3bacdbe8 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/GetTeamResponse.java @@ -0,0 +1,34 @@ +package com.moing.backend.domain.team.application.dto.response; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.dto.response.UserProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GetTeamResponse { + private String memberNickName; + private Integer numOfTeam; + private List teamBlocks = new ArrayList<>(); + private UserProperty userProperty; + + public GetTeamResponse(Integer numOfTeam, List teamBlocks) { + this.numOfTeam=numOfTeam; + this.teamBlocks = teamBlocks; + } + public void updateMemberInfo(Member member) { + this.memberNickName=member.getNickName(); + this.userProperty=new UserProperty(member.getGender(), member.getBirthDate()); + + } +} + + diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/ReviewTeamResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/ReviewTeamResponse.java new file mode 100644 index 00000000..f259913a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/ReviewTeamResponse.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ReviewTeamResponse { + + private Long teamId; + private String teamName; + private Integer numOfMember; + private Long duration; //걸린시간(단위:날짜) + private Long numOfMission; + private Integer levelOfFire; //불꽃 레벨 + private Boolean isLeader; + private String memberName; +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamBlock.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamBlock.java new file mode 100644 index 00000000..46b78bc3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamBlock.java @@ -0,0 +1,54 @@ +package com.moing.backend.domain.team.application.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +@Getter +@AllArgsConstructor +@Builder +@NoArgsConstructor +public class TeamBlock { + + private Long teamId; + private Long duration; //걸린시간(단위:날짜) + private Integer levelOfFire; //불꽃 레벨 + private String teamName; + private Integer numOfMember; + private String category; + private String startDate; + private LocalDateTime deletionTime; + private String profileImgUrl; + + @QueryProjection + public TeamBlock(Long teamId, LocalDateTime approvalTime, Integer levelOfFire, String teamName, Integer numOfMember, String category, LocalDateTime deletionTime, String profileImgUrl) { + this.teamId=teamId; + this.duration=calculateDuration(approvalTime); + this.levelOfFire=levelOfFire; + this.teamName=teamName; + this.numOfMember=numOfMember; + this.category = category; + this.startDate=approvalTime.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + this.deletionTime=deletionTime; + this.profileImgUrl = profileImgUrl; + } + + public Long calculateDuration(LocalDateTime approvalTime) { + ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); + LocalDateTime currentDateTime = LocalDateTime.now(seoulZoneId); + + long hoursBetween = ChronoUnit.HOURS.between(approvalTime, currentDateTime); + long daysBetween = hoursBetween / 24; + + return daysBetween; + } + +} + diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamInfo.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamInfo.java new file mode 100644 index 00000000..5fb2e67e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamInfo.java @@ -0,0 +1,23 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@Getter +@Builder +public class TeamInfo { + private Boolean isDeleted; + private LocalDateTime deletionTime; + private String teamName; //소모임 이름 + private Integer numOfMember; //소모임원 수 + private String category; //카테고리 + private String introduction; //소개 + private Long currentUserId; //현재 유저 아이디 + private List teamMemberInfoList = new ArrayList<>(); //소모임원 정보 +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamMemberInfo.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamMemberInfo.java new file mode 100644 index 00000000..cc42df33 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/TeamMemberInfo.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.team.application.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class TeamMemberInfo { + private Long memberId; + private String nickName; + private String profileImage; + private String introduction; + private Boolean isLeader; + + @QueryProjection + public TeamMemberInfo(Long memberId, String nickName, String profileImage, String introduction, Long leaderId){ + this.memberId=memberId; + this.nickName=nickName; + this.profileImage=profileImage; + this.introduction=introduction; + this.isLeader=memberId.equals(leaderId); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/dto/response/UpdateTeamResponse.java b/src/main/java/com/moing/backend/domain/team/application/dto/response/UpdateTeamResponse.java new file mode 100644 index 00000000..79a15208 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/dto/response/UpdateTeamResponse.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.team.application.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class UpdateTeamResponse { + private Long teamId; +} diff --git a/src/main/java/com/moing/backend/domain/team/application/mapper/TeamMapper.java b/src/main/java/com/moing/backend/domain/team/application/mapper/TeamMapper.java new file mode 100644 index 00000000..a7d4cd69 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/mapper/TeamMapper.java @@ -0,0 +1,72 @@ +package com.moing.backend.domain.team.application.mapper; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.application.dto.request.CreateTeamRequest; +import com.moing.backend.domain.team.application.dto.response.*; +import com.moing.backend.domain.team.domain.constant.ApprovalStatus; +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Component +public class TeamMapper { + + public static Team createTeam(CreateTeamRequest createTeamRequest, Member member) { + return Team.builder() + .category(createTeamRequest.getCategory()) + .name(createTeamRequest.getName()) + .introduction(createTeamRequest.getIntroduction()) + .promise(createTeamRequest.getPromise()) + .profileImgUrl(createTeamRequest.getProfileImgUrl()) + .approvalStatus(ApprovalStatus.NO_CONFIRMATION) + .leaderId(member.getMemberId()) + .numOfMember(0) + .levelOfFire(1) + .build(); + } + + public static GetTeamDetailResponse toTeamDetailResponse(Long memberId, Team team, Integer boardNum, List teamMemberInfoList) { + TeamInfo teamInfo = new TeamInfo(team.isDeleted(), team.getDeletionTime(), team.getName(), teamMemberInfoList.size(), team.getCategory(), team.getIntroduction(), memberId, teamMemberInfoList); + return GetTeamDetailResponse.builder() + .boardNum(boardNum) + .teamInfo(teamInfo) + .build(); + } + + public static ReviewTeamResponse toReviewTeamResponse(Long numOfMission, Team team, boolean isLeader, String memberName){ + return ReviewTeamResponse + .builder() + .teamId(team.getTeamId()) + .teamName(team.getName()) + .numOfMember(team.getNumOfMember()) + .levelOfFire(team.getLevelOfFire()) + .duration(calculateDuration(team.getApprovalTime())) + .numOfMission(numOfMission) + .isLeader(isLeader) + .memberName(memberName) + .build(); + } + + public static Long calculateDuration(LocalDateTime approvalTime) { + ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); + LocalDateTime currentDateTime = LocalDateTime.now(seoulZoneId); + + long hoursBetween = ChronoUnit.HOURS.between(approvalTime, currentDateTime); + long daysBetween = hoursBetween / 24; + + return daysBetween; + } + + public static GetCurrentStatusResponse toCurrentStatusResponse(Team team) { + return GetCurrentStatusResponse.builder() + .name(team.getName()) + .introduction(team.getIntroduction()) + .profileImgUrl(team.getProfileImgUrl()) + .build(); + } + +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/CheckLeaderUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/CheckLeaderUseCase.java new file mode 100644 index 00000000..d3ae2ba7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/CheckLeaderUseCase.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.exception.NotAuthByTeamException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.Objects; + +@Service +@Transactional +@RequiredArgsConstructor +public class CheckLeaderUseCase { + public boolean isTeamLeader(Member member, Team team) { + return Objects.equals(member.getMemberId(), team.getLeaderId()); + } + + public void validateTeamLeader(Member member, Team team) { + if (!isTeamLeader(member, team)) { + throw new NotAuthByTeamException(); + } + } + +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/CreateTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/CreateTeamUseCase.java new file mode 100644 index 00000000..aa8d49d4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/CreateTeamUseCase.java @@ -0,0 +1,51 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.application.dto.request.CreateTeamRequest; +import com.moing.backend.domain.team.application.dto.response.CreateTeamResponse; +import com.moing.backend.domain.team.application.mapper.TeamMapper; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamSaveService; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberSaveService; +import com.moing.backend.domain.teamScore.application.mapper.TeamScoreMapper; +import com.moing.backend.domain.teamScore.domain.service.TeamScoreSaveService; +import com.moing.backend.global.config.slack.team.dto.TeamCreateEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class CreateTeamUseCase { + + private final MemberGetService memberGetService; + private final TeamSaveService teamSaveService; + private final TeamMemberSaveService teamMemberSaveService; + private final TeamScoreSaveService teamScoreSaveService; + private final ApplicationEventPublisher eventPublisher; + + public CreateTeamResponse createTeam(CreateTeamRequest createTeamRequest, String socialId){ + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = createAndSaveTeam(createTeamRequest, member); + publishTeamCreateEvent(team); + return new CreateTeamResponse(team.getTeamId()); + } + + private Team createAndSaveTeam(CreateTeamRequest createTeamRequest, Member member) { + Team team = TeamMapper.createTeam(createTeamRequest, member); + teamSaveService.saveTeam(team); + teamMemberSaveService.addTeamMember(team, member); + team.approveTeam(); // 승인 처리 + teamScoreSaveService.save(TeamScoreMapper.mapToTeamScore(team)); + return team; + } + + private void publishTeamCreateEvent(Team team) { + eventPublisher.publishEvent(new TeamCreateEvent(team.getName(), team.getLeaderId())); + } +} + diff --git a/src/main/java/com/moing/backend/domain/team/application/service/DisbandTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/DisbandTeamUseCase.java new file mode 100644 index 00000000..cf6eb651 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/DisbandTeamUseCase.java @@ -0,0 +1,36 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.application.dto.response.DeleteTeamResponse; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class DisbandTeamUseCase { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final CheckLeaderUseCase checkLeaderUseCase; + private final TeamMemberGetService teamMemberGetService; + + public DeleteTeamResponse disbandTeam(String socialId, Long teamId) { + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = teamGetService.getTeamByTeamId(teamId); + checkLeaderUseCase.validateTeamLeader(member, team); + team.deleteTeam(); + if (team.getNumOfMember() == 1) { // 1명인 경우 3일 유예기간 없음 + TeamMember teamMember = teamMemberGetService.getTeamMemberNotDeleted(member, team); + teamMember.deleteMember(team); + } + return new DeleteTeamResponse(teamId); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/GetTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/GetTeamUseCase.java new file mode 100644 index 00000000..3562ae11 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/GetTeamUseCase.java @@ -0,0 +1,56 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.board.domain.service.BoardGetService; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.application.dto.response.*; +import com.moing.backend.domain.team.application.mapper.TeamMapper; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class GetTeamUseCase { + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final BoardGetService boardGetService; + private final TeamMemberGetService teamMemberGetService; + + public GetTeamResponse getTeam(String socialId) { + Member member = memberGetService.getMemberBySocialId(socialId); + return teamGetService.getTeamByMember(member); + } + + public GetTeamDetailResponse getTeamDetailResponse(String socialId, Long teamId) { + Member member = memberGetService.getMemberBySocialId(socialId); + Integer boardNum = boardGetService.getUnReadBoardNum(teamId, member.getMemberId()); + List teamMemberInfoList = teamMemberGetService.getTeamMemberInfo(member.getMemberId(), teamId); + Team team = teamGetService.getTeamByTeamId(teamId); + return TeamMapper.toTeamDetailResponse(member.getMemberId(), team, boardNum, teamMemberInfoList); + } + + public GetCurrentStatusResponse getCurrentStatus(Long teamId) { + Team team=teamGetService.getTeamByTeamId(teamId); + return TeamMapper.toCurrentStatusResponse(team); + } + + public Page getNewTeam(String dateSort, Pageable pageable) { + return teamGetService.getNewTeams(dateSort, pageable); + } + + public GetTeamCountResponse getTeamCount(String socialId, Long teamId) { + Member member = memberGetService.getMemberBySocialId(socialId); + GetTeamCountResponse response= teamGetService.getTeamCountAndName(teamId, member.getMemberId()); + response.updateMemberName(member.getNickName()); + return response; + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/RejectTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/RejectTeamUseCase.java new file mode 100644 index 00000000..8d4a3c3e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/RejectTeamUseCase.java @@ -0,0 +1,37 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.team.application.dto.response.GetLeaderInfoResponse; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.team.domain.service.TeamUpdateService; +import com.moing.backend.global.config.fcm.dto.event.SingleFcmEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.List; + +import static com.moing.backend.domain.history.domain.entity.PagePath.HOME_PATH; +import static com.moing.backend.global.config.fcm.constant.ApproveTeamMessage.REJECT_TEAM_MESSAGE; + +@Service +@RequiredArgsConstructor +@Transactional +public class RejectTeamUseCase { + + private final TeamUpdateService teamUpdateService; + private final TeamGetService teamGetService; + private final ApplicationEventPublisher eventPublisher; + + public void rejectTeams(List teamIds) { + teamUpdateService.updateTeamStatus(false, teamIds); + List leaderInfos = teamGetService.getLeaderInfoResponses(teamIds); + for (GetLeaderInfoResponse info : leaderInfos) { + String title = REJECT_TEAM_MESSAGE.title(info.getLeaderName(), info.getTeamName()); + String body = REJECT_TEAM_MESSAGE.body(); + +// eventPublisher.publishEvent(new SingleFcmEvent(info.getLeaderFcmToken(), title, body, info.getLeaderId(), "", info.getTeamName(), AlarmType.REJECT_TEAM, HOME_PATH.getValue())); + } + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/ReviewTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/ReviewTeamUseCase.java new file mode 100644 index 00000000..47d85194 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/ReviewTeamUseCase.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.team.application.dto.response.ReviewTeamResponse; +import com.moing.backend.domain.team.application.mapper.TeamMapper; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class ReviewTeamUseCase { + + private final TeamGetService teamGetService; + private final MissionQueryService missionQueryService; + private final CheckLeaderUseCase checkLeaderUseCase; + private final MemberGetService memberGetService; + + public ReviewTeamResponse reviewTeam(String socialId, Long teamId){ + Team team=teamGetService.getTeamByTeamId(teamId); + Member member=memberGetService.getMemberBySocialId(socialId); + boolean isLeader=checkLeaderUseCase.isTeamLeader(member, team); + return TeamMapper.toReviewTeamResponse(missionQueryService.findMissionsCountByTeam(team.getTeamId()),team, isLeader, member.getNickName()); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/SignInTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/SignInTeamUseCase.java new file mode 100644 index 00000000..f40495f3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/SignInTeamUseCase.java @@ -0,0 +1,30 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.application.dto.response.CreateTeamResponse; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.team.exception.DeletedTeamException; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberSaveService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class SignInTeamUseCase { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final TeamMemberSaveService teamMemberSaveService; + public CreateTeamResponse signInTeam(String socialId, Long teamId){ + Member member=memberGetService.getMemberBySocialId(socialId); + Team team=teamGetService.getTeamIncludeDeletedByTeamId(teamId); + teamMemberSaveService.addTeamMember(team, member); + return new CreateTeamResponse(team.getTeamId()); + } + +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/UpdateTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/UpdateTeamUseCase.java new file mode 100644 index 00000000..0564f58d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/UpdateTeamUseCase.java @@ -0,0 +1,42 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.application.dto.request.UpdateTeamRequest; +import com.moing.backend.domain.team.application.dto.response.UpdateTeamResponse; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.global.utils.UpdateUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class UpdateTeamUseCase { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final CheckLeaderUseCase checkLeaderUseCase; + private final UpdateUtils updateUtils; + + public UpdateTeamResponse updateTeam(UpdateTeamRequest updateTeamRequest, String socialId, Long teamId) { + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = teamGetService.getTeamByTeamId(teamId); + + checkLeaderUseCase.validateTeamLeader(member, team); + + String oldProfileImgUrl = team.getProfileImgUrl(); + team.updateTeam( + UpdateUtils.getUpdatedValue(updateTeamRequest.getName(), team.getName()), + UpdateUtils.getUpdatedValue(updateTeamRequest.getIntroduction(), team.getIntroduction()), + UpdateUtils.getUpdatedValue(updateTeamRequest.getProfileImgUrl(), team.getProfileImgUrl()) + ); + + updateUtils.deleteOldImgUrl(updateTeamRequest.getProfileImgUrl(), oldProfileImgUrl); + + return new UpdateTeamResponse(team.getTeamId()); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/application/service/WithdrawTeamUseCase.java b/src/main/java/com/moing/backend/domain/team/application/service/WithdrawTeamUseCase.java new file mode 100644 index 00000000..4e69e4d7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/application/service/WithdrawTeamUseCase.java @@ -0,0 +1,31 @@ +package com.moing.backend.domain.team.application.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.application.dto.response.DeleteTeamResponse; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class WithdrawTeamUseCase { + + private final TeamMemberGetService teamMemberGetService; + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + + public DeleteTeamResponse withdrawTeam(String socialId, Long teamId) { + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = teamGetService.getTeamByTeamId(teamId); + TeamMember teamMember = teamMemberGetService.getTeamMemberNotDeleted(member, team); + teamMember.deleteMember(team); + return new DeleteTeamResponse(teamId); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/constant/ApprovalStatus.java b/src/main/java/com/moing/backend/domain/team/domain/constant/ApprovalStatus.java new file mode 100644 index 00000000..3a5695dd --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/constant/ApprovalStatus.java @@ -0,0 +1,14 @@ +package com.moing.backend.domain.team.domain.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@AllArgsConstructor +public enum ApprovalStatus { + NO_CONFIRMATION("확인안함"), + APPROVAL("승인"), + REJECTION("거절"); + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/constant/Category.java b/src/main/java/com/moing/backend/domain/team/domain/constant/Category.java new file mode 100644 index 00000000..d50e7085 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/constant/Category.java @@ -0,0 +1,18 @@ +//package com.moing.backend.domain.team.domain.constant; +// +//import lombok.Getter; +//import lombok.RequiredArgsConstructor; +// +//@Getter +//@RequiredArgsConstructor +//public enum Category{ +// SPORTS("스포츠/운동"), +// HABIT("생활습관 개선"), +// TEST("시험/취업준비"), +// STUDY("스터디/공부"), +// READING("독서"), +// ETC("그외 자기계발"); +// +// private final String message; +//} +// diff --git a/src/main/java/com/moing/backend/domain/team/domain/entity/Team.java b/src/main/java/com/moing/backend/domain/team/domain/entity/Team.java new file mode 100644 index 00000000..cb348a9c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/entity/Team.java @@ -0,0 +1,96 @@ +package com.moing.backend.domain.team.domain.entity; + +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.team.domain.constant.ApprovalStatus; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Entity +public class Team extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "team_id") + private Long teamId; + + @Column(nullable = false, length = 10) + private String category; + + @Column(nullable = false, length = 10) + private String name; + + @Column(nullable = false) + private String profileImgUrl; + + @Column(nullable = false, length = 300) + private String introduction; + + @Column(nullable = false, length = 100) + private String promise; + + @Column(nullable = false) + private Long leaderId; + + private String invitationCode; + + @Column(nullable = false, length = 16) + @Enumerated(EnumType.STRING) + private ApprovalStatus approvalStatus; + + private LocalDateTime approvalTime; + + private boolean isDeleted; + + private LocalDateTime deletionTime; + + private Integer numOfMember; //반정규화 + private Integer levelOfFire; + + @OneToMany(mappedBy = "team") + List missions = new ArrayList<>(); + + public void approveTeam() { + this.approvalStatus = ApprovalStatus.APPROVAL; + this.approvalTime = LocalDateTime.now(ZoneId.of("Asia/Seoul")).withNano(0); + } + + public void rejectTeam() { + this.approvalStatus = ApprovalStatus.REJECTION; + } + + public void updateTeam(String name, String introduction, String profileImgUrl) { + this.name = name; + this.introduction = introduction; + this.profileImgUrl = profileImgUrl; + } + + public void deleteTeam() { + this.isDeleted=true; + this.deletionTime = LocalDateTime.now().withNano(0); + } + + public void addTeamMember(){ + numOfMember++; + } + + public void deleteTeamMember(){ + numOfMember--; + } + + public void updateLevelOfFire(Integer level) { + this.levelOfFire = level; + } +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/repository/TeamCustomRepository.java b/src/main/java/com/moing/backend/domain/team/domain/repository/TeamCustomRepository.java new file mode 100644 index 00000000..6d3f3ba3 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/repository/TeamCustomRepository.java @@ -0,0 +1,29 @@ +package com.moing.backend.domain.team.domain.repository; + +import com.moing.backend.domain.missionArchive.application.dto.res.MyTeamsRes; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageTeamBlock; +import com.moing.backend.domain.team.application.dto.response.GetLeaderInfoResponse; +import com.moing.backend.domain.team.application.dto.response.GetNewTeamResponse; +import com.moing.backend.domain.team.application.dto.response.GetTeamCountResponse; +import com.moing.backend.domain.team.application.dto.response.GetTeamResponse; +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface TeamCustomRepository { + GetTeamResponse findTeamByMemberId(Long memberId); + Optional findTeamByTeamId(Long TeamId); + Optional findTeamIncludeDeletedByTeamId(Long teamId); + List findTeamIdByMemberId(Long memberId); + List findMyPageTeamByMemberId(Long memberId); + List findTeamNameByTeamId(List teamId); + void updateTeamStatus(boolean isApproved, List teamIds); + List findLeaderInfoByTeamIds(List teamIds); + Page findNewTeam(String dateSort, Pageable pageable); + GetTeamCountResponse findTeamCount(Long memberId, Long teamId); + Long getTodayNewTeams(); + Long getYesterdayNewTeams(); +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/repository/TeamCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/team/domain/repository/TeamCustomRepositoryImpl.java new file mode 100644 index 00000000..21cfeeb5 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/repository/TeamCustomRepositoryImpl.java @@ -0,0 +1,256 @@ +package com.moing.backend.domain.team.domain.repository; + +import com.moing.backend.domain.missionArchive.application.dto.res.MyTeamsRes; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageTeamBlock; +import com.moing.backend.domain.team.application.dto.response.*; +import com.moing.backend.domain.team.domain.constant.ApprovalStatus; +import com.moing.backend.domain.team.domain.entity.Team; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.querydsl.jpa.impl.JPAUpdateClause; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import javax.persistence.EntityManager; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.member.domain.entity.QMember.member; +import static com.moing.backend.domain.team.domain.entity.QTeam.team; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; + +public class TeamCustomRepositoryImpl implements TeamCustomRepository { + + private final JPAQueryFactory queryFactory; + private final EntityManager em; + + public TeamCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + this.em = em; + } + + @Override + public GetTeamResponse findTeamByMemberId(Long memberId) { + List teamBlocks = getTeamBlock(memberId); + Integer numOfTeam = teamBlocks.size(); + return new GetTeamResponse(numOfTeam, teamBlocks); + } + + @Override + public Optional findTeamByTeamId(Long teamId) { + LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(3); + return Optional.ofNullable(queryFactory.selectFrom(team) + .where(team.teamId.eq(teamId)) + .where(team.isDeleted.eq(false) // 강제종료되지 않았거나 + .or(team.deletionTime.after(threeDaysAgo))) // 강제종료된 경우 3일이 지나지 않았다면 + .fetchOne()); + } + + @Override + public Optional findTeamIncludeDeletedByTeamId(Long teamId){ + return Optional.ofNullable(queryFactory.selectFrom(team) + .where(team.teamId.eq(teamId)) + .fetchOne()); + } + + @Override + public List findTeamIdByMemberId(Long memberId){ + LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(3); + + return queryFactory + .select(team.teamId) + .from(teamMember) + .innerJoin(teamMember.team, team) + .on(teamMember.member.memberId.eq(memberId)) + .where(team.approvalStatus.eq(ApprovalStatus.APPROVAL)) // 승인 되었고 + .where(teamMember.isDeleted.eq(false)) // 탈퇴하지 않았다면 + .where(team.isDeleted.eq(false) // 강제종료되지 않았거나 + .or(team.deletionTime.after(threeDaysAgo))) // 강제종료된 경우 3일이 지나지 않았다면 + .groupBy(team.teamId) + .orderBy(team.approvalTime.asc()) + .fetch(); + } + + @Override + public List findMyPageTeamByMemberId(Long memberId) { + return queryFactory + .select(Projections.constructor(GetMyPageTeamBlock.class, + team.teamId, team.name, team.category, team.profileImgUrl)) + .from(teamMember) + .innerJoin(teamMember.team, team) + .on(teamMember.member.memberId.eq(memberId)) + .where(team.approvalStatus.eq(ApprovalStatus.APPROVAL)) // 승인 되었고 + .orderBy(team.missions.size().desc()) + .groupBy(team.teamId) + .fetch(); + } + + private List getTeamBlock(Long memberId) { + LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(3); + + return queryFactory + .select(new QTeamBlock(team.teamId, + team.approvalTime, + team.levelOfFire, + team.name, + team.numOfMember, + team.category, + team.deletionTime, + team.profileImgUrl)) + .from(teamMember) + .innerJoin(teamMember.team, team) + .on(teamMember.member.memberId.eq(memberId)) + .where(team.approvalStatus.eq(ApprovalStatus.APPROVAL)) // 승인 되었고 + .where(teamMember.isDeleted.eq(false)// 탈퇴하지 않았다면 + .and(team.isDeleted.eq(false) // 강제종료되지 않았거나 + .or(team.deletionTime.after(threeDaysAgo))))// 강제종료된 경우 3일이 지나지 않았다면 + .orderBy(team.approvalTime.asc()) + .groupBy(team.teamId) + .fetch(); + } + + @Override + public List findTeamNameByTeamId(List teamId) { + return queryFactory + .select(Projections.constructor(MyTeamsRes.class, + team.teamId, + team.name)) + .from(team) + .where(team.teamId.in(teamId)) + .fetch(); + } + + @Override + public void updateTeamStatus(boolean isApproved, List teamIds) { + ApprovalStatus approvalStatus = isApproved ? ApprovalStatus.APPROVAL : ApprovalStatus.REJECTION; + + JPAUpdateClause updateClause = queryFactory + .update(team) + .set(team.approvalStatus, approvalStatus); + + // 승인되었을 때만 현재 시간으로 approvalTime 설정 + if (isApproved) { + updateClause.set(team.approvalTime, LocalDateTime.now()); + } + + updateClause + .where(team.teamId.in(teamIds)) + .execute(); + + em.flush(); + em.clear(); + } + + @Override + public List findLeaderInfoByTeamIds(List teamIds) { + return queryFactory + .select(Projections.constructor(GetLeaderInfoResponse.class, + team.teamId, + team.name, + member.memberId, + member.nickName, + member.fcmToken)) + .from(team) + .join(member).on(team.leaderId.eq(member.memberId)) + .where(team.teamId.in(teamIds)) + .fetch(); + } + + @Override + public Page findNewTeam(String dateSort, Pageable pageable) { + OrderSpecifier orderBy = team.createdDate.asc(); + if ("desc".equals(dateSort)) { + orderBy = team.createdDate.desc(); + } + + List teams = queryFactory + .select(Projections.constructor(GetNewTeamResponse.class, + team.name, team.category, team.promise, team.introduction, team.profileImgUrl, team.createdDate)) + .from(team) + .where(team.approvalStatus.eq(ApprovalStatus.NO_CONFIRMATION)) + .orderBy(orderBy) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long count = queryFactory + .select(team.count()) + .from(team) + .where(team.approvalStatus.eq(ApprovalStatus.NO_CONFIRMATION)) + .fetchOne(); + + return PageableExecutionUtils.getPage(teams, pageable, () -> count); + } + + @Override + public GetTeamCountResponse findTeamCount(Long memberId, Long teamId) { + LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(3); + Long count=queryFactory + .select(team.count()) // 팀의 개수를 세기 위해 수정 + .from(teamMember) + .innerJoin(teamMember.team, team) + .on(teamMember.member.memberId.eq(memberId)) + .where(team.approvalStatus.eq(ApprovalStatus.APPROVAL)) // 승인 되었고 + .where(teamMember.isDeleted.eq(false) // 탈퇴하지 않았다면 + .and(team.isDeleted.eq(false) // 강제종료되지 않았거나 + .or(team.deletionTime.after(threeDaysAgo)))) // 강제종료된 경우 3일이 지나지 않았다면 + .fetchOne(); // 단일 결과 (개수) 반환 + + if (count == null) { + count = 0L; // null인 경우 0으로 처리 + } + + GetTeamCountResponse response=queryFactory + .select(new QGetTeamCountResponse(team.name, member.nickName)) + .from(team) + .join(member).on(team.leaderId.eq(member.memberId)) + .where(team.teamId.eq(teamId)) + .fetchOne(); + + if (response == null) { + // response가 null인 경우, 적절한 기본값 설정 또는 예외 처리 + response = new GetTeamCountResponse("기본 팀 이름", "기본 멤버 닉네임"); + response.updateCount(0L); + } else { + response.updateCount(count); + } + + return response; + } + + @Override + public Long getTodayNewTeams() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long todayNewTeams = queryFactory + .selectFrom(team) + .where(team.createdDate.between(startOfToday, endOfToday)) + .fetchCount(); + return todayNewTeams; + + } + + @Override + public Long getYesterdayNewTeams() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + LocalDateTime startOfToday = now.toLocalDate().atStartOfDay(); + LocalDateTime endOfToday = startOfToday.plusDays(1); + LocalDateTime startOfYesterday = startOfToday.minusDays(1); + + long yesterdayNewTeams = queryFactory + .selectFrom(team) + .where(team.createdDate.between(startOfYesterday, startOfToday)) + .fetchCount(); + + return yesterdayNewTeams; + } + + +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/repository/TeamRepository.java b/src/main/java/com/moing/backend/domain/team/domain/repository/TeamRepository.java new file mode 100644 index 00000000..709ad833 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/repository/TeamRepository.java @@ -0,0 +1,9 @@ +package com.moing.backend.domain.team.domain.repository; + +import com.moing.backend.domain.team.domain.entity.Team; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TeamRepository extends JpaRepository, TeamCustomRepository { +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/service/TeamGetService.java b/src/main/java/com/moing/backend/domain/team/domain/service/TeamGetService.java new file mode 100644 index 00000000..a89d672e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/service/TeamGetService.java @@ -0,0 +1,75 @@ +package com.moing.backend.domain.team.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.missionArchive.application.dto.res.MyTeamsRes; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageTeamBlock; +import com.moing.backend.domain.team.application.dto.response.GetLeaderInfoResponse; +import com.moing.backend.domain.team.application.dto.response.GetNewTeamResponse; +import com.moing.backend.domain.team.application.dto.response.GetTeamCountResponse; +import com.moing.backend.domain.team.application.dto.response.GetTeamResponse; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.repository.TeamRepository; +import com.moing.backend.domain.team.exception.NotFoundByTeamIdException; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.List; + +@DomainService +@RequiredArgsConstructor +public class TeamGetService { + private final TeamRepository teamRepository; + + public GetTeamResponse getTeamByMember(Member member) { + GetTeamResponse getTeamResponse = teamRepository.findTeamByMemberId(member.getMemberId()); + getTeamResponse.updateMemberInfo(member); + return getTeamResponse; + } + + public List getTeamIdByMemberId(Long memberId) { + return teamRepository.findTeamIdByMemberId(memberId); + } + + public Team getTeamIncludeDeletedByTeamId(Long teamId){ + return teamRepository.findTeamIncludeDeletedByTeamId(teamId).orElseThrow(NotFoundByTeamIdException::new); + } + + public Team getTeamByTeamId(Long teamId){ + return teamRepository.findTeamByTeamId(teamId).orElseThrow(NotFoundByTeamIdException::new); + } + + public List getMyPageTeamBlockByMemberId(Long memberId){ + return teamRepository.findMyPageTeamByMemberId(memberId); + } + + public List getTeamNameByTeamId(List teamId) { + return teamRepository.findTeamNameByTeamId(teamId); + } + + public List getLeaderInfoResponses(List teamIds){ + return teamRepository.findLeaderInfoByTeamIds(teamIds); + } + + public Page getNewTeams(String dateSort, Pageable pageable) { + int page = (pageable.getPageNumber() == 0) ? 0 : (pageable.getPageNumber() - 1); + pageable = PageRequest.of(page, pageable.getPageSize(), Sort.by("no").descending()); + return teamRepository.findNewTeam(dateSort, pageable); + } + + public GetTeamCountResponse getTeamCountAndName(Long teamId, Long memberId) { + + return teamRepository.findTeamCount(memberId, teamId); + } + + public Long getTodayNewTeams(){ + return teamRepository.getTodayNewTeams(); + } + + public Long getYesterdayNewTeams(){ + return teamRepository.getYesterdayNewTeams(); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/service/TeamSaveService.java b/src/main/java/com/moing/backend/domain/team/domain/service/TeamSaveService.java new file mode 100644 index 00000000..11a48024 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/service/TeamSaveService.java @@ -0,0 +1,15 @@ +package com.moing.backend.domain.team.domain.service; + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.repository.TeamRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +@DomainService +@RequiredArgsConstructor +public class TeamSaveService { + private final TeamRepository teamRepository; + public void saveTeam(Team team){ + teamRepository.save(team); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/domain/service/TeamUpdateService.java b/src/main/java/com/moing/backend/domain/team/domain/service/TeamUpdateService.java new file mode 100644 index 00000000..e7563277 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/domain/service/TeamUpdateService.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.team.domain.service; + +import com.moing.backend.domain.team.domain.repository.TeamRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.List; + +@DomainService +@RequiredArgsConstructor +public class TeamUpdateService { + + private final TeamRepository teamRepository; + private final ApplicationEventPublisher eventPublisher; + public void updateTeamStatus(boolean isApproved, List teamIds){ + teamRepository.updateTeamStatus(isApproved, teamIds); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/exception/AlreadyJoinTeamException.java b/src/main/java/com/moing/backend/domain/team/exception/AlreadyJoinTeamException.java new file mode 100644 index 00000000..4c12a0ac --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/exception/AlreadyJoinTeamException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.team.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class AlreadyJoinTeamException extends TeamException{ + public AlreadyJoinTeamException(){ + super(ErrorCode.ALREADY_JOIN_ERROR, + HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/exception/AlreadyWithdrawTeamException.java b/src/main/java/com/moing/backend/domain/team/exception/AlreadyWithdrawTeamException.java new file mode 100644 index 00000000..218d7e3b --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/exception/AlreadyWithdrawTeamException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.team.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class AlreadyWithdrawTeamException extends TeamException{ + public AlreadyWithdrawTeamException(){ + super(ErrorCode.ALREADY_WITHDRAW_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/exception/DeletedTeamException.java b/src/main/java/com/moing/backend/domain/team/exception/DeletedTeamException.java new file mode 100644 index 00000000..53bfdd47 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/exception/DeletedTeamException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.team.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class DeletedTeamException extends TeamException{ + public DeletedTeamException(){ + super(ErrorCode.DELETED_TEAM_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/exception/NotAuthByTeamException.java b/src/main/java/com/moing/backend/domain/team/exception/NotAuthByTeamException.java new file mode 100644 index 00000000..7d99e8cb --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/exception/NotAuthByTeamException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.team.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotAuthByTeamException extends TeamException{ + public NotAuthByTeamException(){ + super(ErrorCode.NOT_AUTH_BY_TEAM_ERROR, + HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/exception/NotFoundByTeamIdException.java b/src/main/java/com/moing/backend/domain/team/exception/NotFoundByTeamIdException.java new file mode 100644 index 00000000..cdedc03f --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/exception/NotFoundByTeamIdException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.team.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundByTeamIdException extends TeamException{ + public NotFoundByTeamIdException(){ + super(ErrorCode.NOT_FOUND_BY_TEAM_ID_ERROR, + HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/moing/backend/domain/team/exception/TeamException.java b/src/main/java/com/moing/backend/domain/team/exception/TeamException.java new file mode 100644 index 00000000..e7b8006e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/exception/TeamException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.team.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class TeamException extends ApplicationException { + protected TeamException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/team/presentation/TeamController.java b/src/main/java/com/moing/backend/domain/team/presentation/TeamController.java new file mode 100644 index 00000000..12de51b9 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/presentation/TeamController.java @@ -0,0 +1,152 @@ +package com.moing.backend.domain.team.presentation; + +import com.moing.backend.domain.team.application.dto.request.CreateTeamRequest; +import com.moing.backend.domain.team.application.dto.request.UpdateTeamRequest; +import com.moing.backend.domain.team.application.dto.response.*; +import com.moing.backend.domain.team.application.service.*; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +import static com.moing.backend.domain.team.presentation.constant.TeamResponseMessage.*; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team") +public class TeamController { + private final CreateTeamUseCase createTeamService; + private final GetTeamUseCase getTeamUseCase; + private final SignInTeamUseCase signInTeamUseCase; + private final DisbandTeamUseCase disbandTeamUseCase; + private final WithdrawTeamUseCase withdrawTeamUseCase; + private final UpdateTeamUseCase updateTeamUseCase; + private final ReviewTeamUseCase reviewTeamUseCase; + + /** + * 소모임 생성 (only 개설만) + * [POST] api/team + * 작성자 : 김민수 + */ + @PostMapping + public ResponseEntity> createTeam(@AuthenticationPrincipal User user, + @Valid @RequestBody CreateTeamRequest createTeamRequest) { + return ResponseEntity.ok(SuccessResponse.create(CREATE_TEAM_SUCCESS.getMessage(), this.createTeamService.createTeam(createTeamRequest,user.getSocialId()))); + } + + /** + * 소모임 전체 조회하기 (소모임 홈화면) : 인증사진 제외 + * [GET] api/team + * 작성자 : 김민수 + */ + @GetMapping + public ResponseEntity> getTeam(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_TEAM_SUCCESS.getMessage(), this.getTeamUseCase.getTeam(user.getSocialId()))); + } + + /** + * 소모임 하나만 조회하기 (목표보드) : 소모임 레벨, 상태 바 제외 + * [GET] api/team/{teamId} + * 작성자: 김민수 + */ + @GetMapping("/board/{teamId}") + public ResponseEntity> getTeamDetail(@AuthenticationPrincipal User user, + @PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(GET_TEAM_DETAIL_SUCCESS.getMessage(), this.getTeamUseCase.getTeamDetailResponse(user.getSocialId(), teamId))); + } + + /** + * 소모임 가입하기 (소모임원으로 입장) + * [POST] api/team/{teamId} + * 작성자 : 김민수 + */ + + @PostMapping("/{teamId}") + public ResponseEntity> signInTeam(@AuthenticationPrincipal User user, + @PathVariable Long teamId){ + return ResponseEntity.ok(SuccessResponse.create(SIGNIN_TEAM_SUCCESS.getMessage(),this.signInTeamUseCase.signInTeam(user.getSocialId(), teamId))); + } + + /** + * 소모임 강제 종료 (소모임장 권한) + * [DELETE] api/team/{teamId}/disband + * 작성자:김민수 + */ + @DeleteMapping("/{teamId}/disband") + public ResponseEntity> disbandTeam(@AuthenticationPrincipal User user, + @PathVariable Long teamId){ + return ResponseEntity.ok(SuccessResponse.create(DISBAND_TEAM_SUCCESS.getMessage(), this.disbandTeamUseCase.disbandTeam(user.getSocialId(), teamId))); + } + + /** + * 소모임 탈퇴 (소모임원) + * [DELETE] api/team/{teamId}/withdraw + * 작성자: 김민수 + */ + @DeleteMapping("/{teamId}/withdraw") + public ResponseEntity> withdrawTeam(@AuthenticationPrincipal User user, + @PathVariable Long teamId){ + return ResponseEntity.ok(SuccessResponse.create(WITHDRAW_TEAM_SUCCESS.getMessage(), this.withdrawTeamUseCase.withdrawTeam(user.getSocialId(),teamId))); + } + + /** + * 소모임 강제 종료, 탈퇴 전 정보 보여주기 + * [GET] api/team/{teamId}/review + * 작성자: 김민수 + */ + @GetMapping("/{teamId}/review") + public ResponseEntity> reviewTeam(@AuthenticationPrincipal User user, + @PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(REVIEW_TEAM_SUCCESS.getMessage(), this.reviewTeamUseCase.reviewTeam(user.getSocialId(), teamId))); + } + + + /** + * 소모임 수정 (소모임장) + * [POST] api/team/{teamId} + */ + + @PutMapping("/{teamId}") + public ResponseEntity> updateTeam(@Valid @RequestBody UpdateTeamRequest updateTeamRequest, + @AuthenticationPrincipal User user, + @PathVariable Long teamId){ + return ResponseEntity.ok(SuccessResponse.create(UPDATE_TEAM_SUCCESS.getMessage(), this.updateTeamUseCase.updateTeam(updateTeamRequest, user.getSocialId(), teamId))); + } + + /** + * 소모임 수정 전 조회 + * [GET] api/team/{teamid} + */ + @GetMapping("/{teamId}") + public ResponseEntity> getCurrentStatus(@AuthenticationPrincipal User user, + @PathVariable Long teamId) { + return ResponseEntity.ok(SuccessResponse.create(GET_CURRENT_STATUS_SUCCESS.getMessage(), this.getTeamUseCase.getCurrentStatus(teamId))); + } + + /** + * 소모임 닉네임과 소모임 개수 조회 + * [GET] api/team/{teamId}/count + */ + @GetMapping("/{teamId}/count") + public ResponseEntity> getTeamCount(@AuthenticationPrincipal User user, + @PathVariable Long teamId){ + return ResponseEntity.ok(SuccessResponse.create(GET_TEAM_COUNT_SUCCESS.getMessage(), this.getTeamUseCase.getTeamCount(user.getSocialId(),teamId))); + } + + @PostMapping(value = "/test", name = "테스트") + public void test() { + Thread thread1 = new Thread(() -> { + this.signInTeamUseCase.signInTeam("KAKAO@tester01", 1L); + }); + Thread thread2 = new Thread(() -> { + this.signInTeamUseCase.signInTeam("KAKAO@tester01", 1L); + }); + thread1.start(); + thread2.start(); + } + +} diff --git a/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java b/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java new file mode 100644 index 00000000..c0a1215e --- /dev/null +++ b/src/main/java/com/moing/backend/domain/team/presentation/constant/TeamResponseMessage.java @@ -0,0 +1,23 @@ +package com.moing.backend.domain.team.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TeamResponseMessage { + CREATE_TEAM_SUCCESS("소모임을 생성하였습니다."), + GET_TEAM_SUCCESS("홈 화면에서 내 소모임을 모두 조회했습니다."), + GET_TEAM_DETAIL_SUCCESS("목표보드를 조회했습니다."), + SIGNIN_TEAM_SUCCESS("소모임에 가입하였습니다."), + GET_CURRENT_STATUS_SUCCESS("소모임 수정 전 조회했습니다."), + REVIEW_TEAM_SUCCESS("소모임 삭제 전 조회했습니다."), + DISBAND_TEAM_SUCCESS("[소모임장 권한] 소모임을 강제 종료했습니다."), + UPDATE_TEAM_SUCCESS("[소모임장 권한] 소모임을 수정했습니다"), + WITHDRAW_TEAM_SUCCESS("[소모임원 권한] 소모임을 탈퇴하였습니다"), + SEND_APPROVAL_ALARM_SUCCESS("소모임들이 승인되었습니다."), + SEND_REJECTION_ALARM_SUCCESS("소모임들이 반려되었습니다."), + GET_NEW_TEAM_SUCCESS("새로운 소모임들을 조회했습니다."), + GET_TEAM_COUNT_SUCCESS("소모임의 개수와 닉네임을 조회했습니다."); + private final String message; +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/entity/TeamMember.java b/src/main/java/com/moing/backend/domain/teamMember/domain/entity/TeamMember.java new file mode 100644 index 00000000..d2ef4648 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/entity/TeamMember.java @@ -0,0 +1,55 @@ +package com.moing.backend.domain.teamMember.domain.entity; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TeamMember extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="team_member_id") + private Long teamMemberId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id") + private Team team; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + private boolean isDeleted; //소모임 삭제여부, 마이페이지에서는 상관 없이 모두 조회 + + //==연관관계 메서드 ==// + public void updateTeam(Team team) { + this.team = team; + } + + public void updateMember(Member member) { + this.member = member; + if (member != null) { + member.getTeamMembers().add(this); + } + } + + public void deleteMember(Team team) { + this.isDeleted = true; + team.deleteTeamMember(); + } + + public String getMemberNickName(){ + return member.getNickName(); + } +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java new file mode 100644 index 00000000..54efa9db --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepository.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.teamMember.domain.repository; + +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.team.application.dto.response.TeamMemberInfo; + +import java.util.List; +import java.util.Optional; + +public interface TeamMemberCustomRepository { + Optional> findNewUploadInfo(Long teamId, Long memberId); + List findTeamMemberInfoByTeamId(Long memberId, Long teamId); +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java new file mode 100644 index 00000000..bc789a1a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberCustomRepositoryImpl.java @@ -0,0 +1,66 @@ +package com.moing.backend.domain.teamMember.domain.repository; + +import com.moing.backend.domain.block.domain.repository.BlockRepositoryUtils; +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.team.application.dto.response.QTeamMemberInfo; +import com.moing.backend.domain.team.application.dto.response.TeamMemberInfo; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; +import java.util.List; +import java.util.Optional; + +import static com.moing.backend.domain.team.domain.entity.QTeam.team; +import static com.moing.backend.domain.teamMember.domain.entity.QTeamMember.teamMember; + +public class TeamMemberCustomRepositoryImpl implements TeamMemberCustomRepository { + private final JPAQueryFactory queryFactory; + + public TeamMemberCustomRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Optional> findNewUploadInfo(Long teamId, Long memberId) { + BooleanExpression blockCondition= BlockRepositoryUtils.blockCondition(teamMember.member.memberId, memberId); + + List result = queryFactory.select(Projections.constructor(NewUploadInfo.class, + teamMember.member.fcmToken, + teamMember.member.memberId, + teamMember.member.isNewUploadPush, + teamMember.member.isSignOut)) + .from(teamMember) + .where(teamMember.team.teamId.eq(teamId) + .and(teamMember.member.memberId.ne(memberId)) + .and(teamMember.isDeleted.eq(false) + .and(blockCondition))) + .fetch(); + + return result.isEmpty() ? Optional.empty() : Optional.of(result); + } + + @Override + public List findTeamMemberInfoByTeamId(Long memberId, Long teamId){ + + BooleanExpression blockCondition= BlockRepositoryUtils.blockCondition(memberId, teamMember.member.memberId); + + + return queryFactory + .select(new QTeamMemberInfo( + teamMember.member.memberId, + teamMember.member.nickName, + teamMember.member.profileImage, + teamMember.member.introduction, + teamMember.team.leaderId)) + .from(teamMember) + .innerJoin(teamMember.team, team) // innerJoin을 사용하여 최적화 + .where(teamMember.team.teamId.eq(teamId) // where 절을 하나로 합침 + .and(teamMember.isDeleted.eq(false)) + .and(blockCondition)) + .groupBy(teamMember.member.memberId) + .fetch(); + } + +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberRepository.java b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberRepository.java new file mode 100644 index 00000000..b7ab7bbe --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/repository/TeamMemberRepository.java @@ -0,0 +1,17 @@ +package com.moing.backend.domain.teamMember.domain.repository; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import javax.swing.text.html.Option; +import java.util.Optional; + +public interface TeamMemberRepository extends JpaRepository, TeamMemberCustomRepository{ + + Optional findTeamMemberByTeamAndMember(Team team, Member member); + +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java b/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java new file mode 100644 index 00000000..5c3122db --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberGetService.java @@ -0,0 +1,41 @@ +package com.moing.backend.domain.teamMember.domain.service; + +import com.moing.backend.domain.history.application.dto.response.NewUploadInfo; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.application.dto.response.TeamMemberInfo; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.exception.NotFoundByTeamIdException; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.repository.TeamMemberRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; + +@DomainService +@RequiredArgsConstructor +public class TeamMemberGetService { + private final TeamMemberRepository teamMemberRepository; + + public TeamMember getTeamMember(Member member, Team team){ + return teamMemberRepository.findTeamMemberByTeamAndMember(team, member).orElseThrow(NotFoundByTeamIdException::new); + } + + public TeamMember getTeamMemberNotDeleted(Member member, Team team) { + return teamMemberRepository.findTeamMemberByTeamAndMember(team, member) + .filter(teamMember -> !teamMember.isDeleted()) + .orElseThrow(NotFoundByTeamIdException::new); + } + + + public List getTeamMemberInfo(Long memberId, Long teamId){ + return teamMemberRepository.findTeamMemberInfoByTeamId(memberId, teamId); + } + + public Optional> getNewUploadInfo(Long teamId, Long memberId) { + return teamMemberRepository.findNewUploadInfo(teamId, memberId); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberSaveService.java b/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberSaveService.java new file mode 100644 index 00000000..8aff52e7 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/domain/service/TeamMemberSaveService.java @@ -0,0 +1,52 @@ +package com.moing.backend.domain.teamMember.domain.service; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.exception.AlreadyJoinTeamException; +import com.moing.backend.domain.team.exception.AlreadyWithdrawTeamException; +import com.moing.backend.domain.team.exception.DeletedTeamException; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.repository.TeamMemberRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; + +import javax.transaction.Transactional; +import java.util.Optional; + +@DomainService +@RequiredArgsConstructor +@Transactional +public class TeamMemberSaveService { + private final TeamMemberRepository teamMemberRepository; + public void addTeamMember(Team team, Member member) { + Optional teamMember = teamMemberRepository.findTeamMemberByTeamAndMember(team, member); + checkDeletion(team); + if (teamMember.isPresent()) { + handleExistingMember(teamMember.get()); + } else { + addNewTeamMember(team, member); + } + } + + private void handleExistingMember(TeamMember teamMember) { + if (teamMember.isDeleted()) { + throw new AlreadyWithdrawTeamException(); + } else { + throw new AlreadyJoinTeamException(); + } + } + + private void addNewTeamMember(Team team, Member member) { + TeamMember newMember = new TeamMember(); + newMember.updateMember(member); + newMember.updateTeam(team); + team.addTeamMember(); + this.teamMemberRepository.save(newMember); + } + + private void checkDeletion(Team team){ + if(team.isDeleted()) + throw new DeletedTeamException(); + } + +} diff --git a/src/main/java/com/moing/backend/domain/teamMember/exception/TeamMemberException.java b/src/main/java/com/moing/backend/domain/teamMember/exception/TeamMemberException.java new file mode 100644 index 00000000..d47d2e21 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamMember/exception/TeamMemberException.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.teamMember.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class TeamMemberException extends ApplicationException { + protected TeamMemberException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/application/dto/TeamScoreRes.java b/src/main/java/com/moing/backend/domain/teamScore/application/dto/TeamScoreRes.java new file mode 100644 index 00000000..149cd63c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/application/dto/TeamScoreRes.java @@ -0,0 +1,16 @@ +package com.moing.backend.domain.teamScore.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TeamScoreRes { + + private Long score; + private Long level; +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/application/mapper/TeamScoreMapper.java b/src/main/java/com/moing/backend/domain/teamScore/application/mapper/TeamScoreMapper.java new file mode 100644 index 00000000..7b02e363 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/application/mapper/TeamScoreMapper.java @@ -0,0 +1,19 @@ +package com.moing.backend.domain.teamScore.application.mapper; + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import com.moing.backend.global.annotation.Mapper; +import org.springframework.stereotype.Component; + +@Component +public class TeamScoreMapper { + + public static TeamScore mapToTeamScore(Team team) { + return TeamScore.builder() + .score(0L) + .level(1L) + .team(team) + .build(); + } + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreGetUseCase.java b/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreGetUseCase.java new file mode 100644 index 00000000..18246a4c --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreGetUseCase.java @@ -0,0 +1,34 @@ +package com.moing.backend.domain.teamScore.application.service; + + +import com.moing.backend.domain.teamScore.application.dto.TeamScoreRes; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import com.moing.backend.domain.teamScore.domain.service.TeamScoreQueryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class TeamScoreGetUseCase { + + private final TeamScoreQueryService teamScoreQueryService; + + public TeamScoreRes getTeamScoreInfo(Long teamId) { + + TeamScore teamScore = teamScoreQueryService.findTeamScoreByTeam(teamId); + Long level = teamScore.getLevel(); + Long score = teamScore.getScore(); + + + return TeamScoreRes.builder() + .score(score%100) + .level(level) + .build(); + + } + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreLogicUseCase.java b/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreLogicUseCase.java new file mode 100644 index 00000000..1fc3566d --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreLogicUseCase.java @@ -0,0 +1,80 @@ +//package com.moing.backend.domain.teamScore.application.service; +// +// +//import com.moing.backend.domain.mission.domain.entity.Mission; +//import com.moing.backend.domain.mission.domain.service.MissionQueryService; +//import com.moing.backend.domain.missionState.domain.service.MissionArchiveStateQueryService; +//import com.moing.backend.domain.team.domain.entity.Team; +//import com.moing.backend.domain.team.domain.service.TeamGetService; +//import com.moing.backend.domain.team.domain.service.TeamSaveService; +//import com.moing.backend.domain.teamScore.application.dto.TeamScoreRes; +//import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +//import com.moing.backend.domain.teamScore.domain.service.TeamScoreQueryService; +//import com.moing.backend.domain.teamScore.domain.service.TeamScoreSaveService; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class TeamScoreLogicUseCase { +// +// private final MissionQueryService missionQueryService; +// private final TeamScoreQueryService teamScoreQueryService; +// private final MissionArchiveStateQueryService missionArchiveStateQueryService; +// +// public TeamScoreRes getTeamScoreInfo(Long teamId) { +// +// return TeamScoreRes.builder() +// .score(getScore(teamId)) +// .level(getLevel(teamId)) +// .build() +// ; +// } +// +// @Transactional +// public Long updateTeamScore(Long missionId) { +// Mission mission = missionQueryService.findMissionById(missionId); +// Team team = mission.getTeam(); +// TeamScore teamScore = teamScoreQueryService.findTeamScoreByTeam(team.getTeamId()); +// +// teamScore.updateScore(getScoreByMission(mission)); +// teamScore.levelUp(); +// +// return teamScore.getScore(); +// } +// +// public Long getScore(Long teamId) { +// return teamScoreQueryService.findTeamScoreByTeam(teamId).getScore(); +// } +// +// public Long getLevel(Long teamId) { +// return teamScoreQueryService.findTeamScoreByTeam(teamId).getLevel(); +// } +// +// public Long getScoreByMission(Mission mission) { +// float total = totalPeople(mission); +// float done = donePeople(mission); +// +// if (done == 0) { +// return 0L; +// } else { +// return (long) ((done / total * 100) / 5); +// } +// +// } +// +// public float donePeople(Mission mission) { +// return Float.valueOf(missionArchiveStateQueryService.stateCountByMissionId(mission.getId())); +// } +// +// public float totalPeople(Mission mission) { +// return Float.valueOf(mission.getTeam().getNumOfMember()); +// +// } +// +// +// +// +//} diff --git a/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreUpdateUseCase.java b/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreUpdateUseCase.java new file mode 100644 index 00000000..9b1c548a --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/application/service/TeamScoreUpdateUseCase.java @@ -0,0 +1,76 @@ +package com.moing.backend.domain.teamScore.application.service; + + +import com.moing.backend.domain.mission.domain.entity.Mission; +import com.moing.backend.domain.mission.domain.entity.constant.MissionType; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamScore.domain.entity.ScoreStatus; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import com.moing.backend.domain.teamScore.domain.service.TeamScoreQueryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class TeamScoreUpdateUseCase { + + private final MissionQueryService missionQueryService; + private final TeamScoreQueryService teamScoreQueryService; + + private final float SCORE_BASE_NUM = 10; + private final long BONUS_SCORE_ONCE_MISSION = 1L; + private final long BONUS_SCORE_REPEAT_MISSION = 2L; + + /** + * 매번 미션 인증 시 점수 적립 + */ + public void gainScoreOfArchive(Mission mission, ScoreStatus scoreStatus) { + + + Team team = mission.getTeam(); + TeamScore teamScore = teamScoreQueryService.findTeamScoreByTeam(team.getTeamId()); + + Integer numOfMember = team.getNumOfMember(); + Long gainScore = calculateScoreByArchive(numOfMember); + + teamScore.updateScore(gainScore * scoreStatus.getValue()); + + } + + private Long calculateScoreByArchive(Integer numOfMember) { + float allPeople = Float.valueOf(numOfMember); + return Math.round(SCORE_BASE_NUM/allPeople) * 2L; + } + + /* + * 보너스 점수 적립 + * 한번 미션은 소모임원 마지막 인증 시 호출, 반복 미션은 각 멤버 당 마지막 인증시 호출 + */ + public void gainScoreOfBonus(Mission mission) { + + Team team = mission.getTeam(); + TeamScore teamScore = teamScoreQueryService.findTeamScoreByTeam(team.getTeamId()); + + Integer numOfMember = team.getNumOfMember(); + + + /** + * 한번미션 일 경우 1 * 소모임원 수, 반복미션 일 경우 2점 update + * 한번 미션은 소모임원 마지막 인증 시 호출, 반복 미션은 각 멤버 당 마지막 인증시 호출 + */ + + if (mission.getType().equals(MissionType.ONCE)) { + teamScore.updateScore(BONUS_SCORE_ONCE_MISSION * numOfMember); + } else { + teamScore.updateScore(BONUS_SCORE_REPEAT_MISSION); + } + + } + + + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/entity/ScoreStatus.java b/src/main/java/com/moing/backend/domain/teamScore/domain/entity/ScoreStatus.java new file mode 100644 index 00000000..3471cae4 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/entity/ScoreStatus.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.teamScore.domain.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ScoreStatus { + PLUS(1L), MINUS(-1L); + private final Long value; +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/entity/TeamScore.java b/src/main/java/com/moing/backend/domain/teamScore/domain/entity/TeamScore.java new file mode 100644 index 00000000..061a5100 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/entity/TeamScore.java @@ -0,0 +1,89 @@ +package com.moing.backend.domain.teamScore.domain.entity; + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.global.entity.BaseTimeEntity; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import javax.persistence.*; +import javax.transaction.Transactional; + +@Entity +@Getter +@RequiredArgsConstructor +public class TeamScore extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "teamScore_id") + private Long id; + + @OneToOne + private Team team; + + private Long score; + + private Long level; + + @Builder + public TeamScore(Team team, Long score,Long level) { + this.team = team; + this.score = score; + this.level = level; + } + + public void updateScore(Long score) { + + int newStep = getStep(this.level, this.score + score); + + this.score += score; + + if (this.score < 0) { // 점수 차감 / score down + level down + this.updateLevel(ScoreStatus.MINUS); + } + else { // 점수 획득. score up / score down + level up + + if ((40 + (newStep * 15L) <= this.score)) { // score down + level up + this.updateLevel(ScoreStatus.PLUS); + + } else { // score up +// this.score += score; + } + } + + + } + + public int getStep(Long level, Long score) { + final int[] steps = {1, 2, 26, 46, 71, 121}; + + int index = 0; + for (int i = 5; i > 0; i--) { + if (steps[i-1] <= this.level && this.level < steps[i]) { + index=i-1; + break; + + } + } + return index; + + } + + public void updateLevel(ScoreStatus sign) { + final int[] steps = {1, 2, 26, 46, 71, 121}; + + this.level += sign.getValue(); + this.team.updateLevelOfFire(this.level.intValue()); + + for (int i = 5; i > 0; i--) { + if (steps[i-1] <= this.level && this.level <= steps[i]) { + if ((40 + ((i-1) * 20)) <= score || score < 0) { + this.score -= sign.getValue() * (40 + ((i-1) * 20)); + return; + } + } + } + + } +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreCustomRepository.java b/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreCustomRepository.java new file mode 100644 index 00000000..1b08b750 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreCustomRepository.java @@ -0,0 +1,11 @@ +package com.moing.backend.domain.teamScore.domain.repository; + + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; + +public interface TeamScoreCustomRepository { + + TeamScore findTeamScoreByTeam(Long teamId); + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreCustomRepositoryImpl.java b/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreCustomRepositoryImpl.java new file mode 100644 index 00000000..8e038986 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreCustomRepositoryImpl.java @@ -0,0 +1,28 @@ +package com.moing.backend.domain.teamScore.domain.repository; + +import static com.moing.backend.domain.teamScore.domain.entity.QTeamScore.teamScore; + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import javax.persistence.EntityManager; + +public class TeamScoreCustomRepositoryImpl implements TeamScoreCustomRepository { + + + private final JPAQueryFactory queryFactory; + + public TeamScoreCustomRepositoryImpl(EntityManager entityManager) { + this.queryFactory = new JPAQueryFactory(entityManager); + } + + @Override + public TeamScore findTeamScoreByTeam(Long teamId) { + return queryFactory + .selectFrom(teamScore) + .where(teamScore.team.teamId.eq(teamId)).fetchFirst(); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreRepository.java b/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreRepository.java new file mode 100644 index 00000000..14771e08 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/repository/TeamScoreRepository.java @@ -0,0 +1,7 @@ +package com.moing.backend.domain.teamScore.domain.repository; + +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TeamScoreRepository extends JpaRepository , TeamScoreCustomRepository{ +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/service/TeamScoreQueryService.java b/src/main/java/com/moing/backend/domain/teamScore/domain/service/TeamScoreQueryService.java new file mode 100644 index 00000000..9f1f1e29 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/service/TeamScoreQueryService.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.teamScore.domain.service; + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import com.moing.backend.domain.teamScore.domain.repository.TeamScoreRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + + +@DomainService +@Transactional +@RequiredArgsConstructor +public class TeamScoreQueryService { + + private final TeamScoreRepository teamScoreRepository; + private final TeamGetService teamGetService; + + public TeamScore findTeamScoreByTeam(Long teamId) { + + return teamScoreRepository.findTeamScoreByTeam(teamId); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/domain/service/TeamScoreSaveService.java b/src/main/java/com/moing/backend/domain/teamScore/domain/service/TeamScoreSaveService.java new file mode 100644 index 00000000..869d9c76 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/domain/service/TeamScoreSaveService.java @@ -0,0 +1,26 @@ +package com.moing.backend.domain.teamScore.domain.service; + +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamScore.domain.entity.TeamScore; +import com.moing.backend.domain.teamScore.domain.repository.TeamScoreRepository; +import com.moing.backend.global.annotation.DomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + + +@DomainService +@Transactional +@RequiredArgsConstructor +public class TeamScoreSaveService { + + private final TeamScoreRepository teamScoreRepository; + private final TeamGetService teamGetService; + + public TeamScore save(TeamScore teamScore) { + + return teamScoreRepository.save(teamScore); + } + + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/presentation/TeamScoreController.java b/src/main/java/com/moing/backend/domain/teamScore/presentation/TeamScoreController.java new file mode 100644 index 00000000..4fc93525 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/presentation/TeamScoreController.java @@ -0,0 +1,40 @@ +package com.moing.backend.domain.teamScore.presentation; + +import com.moing.backend.domain.teamScore.application.dto.TeamScoreRes; +import com.moing.backend.domain.teamScore.application.service.TeamScoreGetUseCase; +import com.moing.backend.domain.teamScore.application.service.TeamScoreUpdateUseCase; +import com.moing.backend.global.config.security.dto.User; +import com.moing.backend.global.response.SuccessResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.moing.backend.domain.teamScore.presentation.constant.TeamScoreResponseMessage.GET_TEAMSCORE_SUCCESS; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/team/{teamId}") +public class TeamScoreController { + + private final TeamScoreGetUseCase teamScoreGetUseCase; + + /** + * 팀별 불 레벨/경험치 조회 + * [GET] my-repeat + * 작성자 : 정승연 + */ + + @GetMapping("/my-fire") + public ResponseEntity> getTeamScore(@PathVariable("teamId") Long teamId, @AuthenticationPrincipal User user) { + return ResponseEntity.ok(SuccessResponse.create(GET_TEAMSCORE_SUCCESS.getMessage(), this.teamScoreGetUseCase.getTeamScoreInfo(teamId))); + } + + + + + +} diff --git a/src/main/java/com/moing/backend/domain/teamScore/presentation/constant/TeamScoreResponseMessage.java b/src/main/java/com/moing/backend/domain/teamScore/presentation/constant/TeamScoreResponseMessage.java new file mode 100644 index 00000000..21d75493 --- /dev/null +++ b/src/main/java/com/moing/backend/domain/teamScore/presentation/constant/TeamScoreResponseMessage.java @@ -0,0 +1,13 @@ +package com.moing.backend.domain.teamScore.presentation.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TeamScoreResponseMessage { + GET_TEAMSCORE_SUCCESS("팀 경험치/레벨 조회를 완료 했습니다"); + + private final String message; +} + diff --git a/src/main/java/com/moing/backend/global/annotation/DomainService.java b/src/main/java/com/moing/backend/global/annotation/DomainService.java new file mode 100644 index 00000000..31a2c872 --- /dev/null +++ b/src/main/java/com/moing/backend/global/annotation/DomainService.java @@ -0,0 +1,15 @@ +package com.moing.backend.global.annotation; + +import org.springframework.stereotype.Service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Service +public @interface DomainService { + +} diff --git a/src/main/java/com/moing/backend/global/annotation/Enum.java b/src/main/java/com/moing/backend/global/annotation/Enum.java new file mode 100644 index 00000000..b804e2ac --- /dev/null +++ b/src/main/java/com/moing/backend/global/annotation/Enum.java @@ -0,0 +1,19 @@ +package com.moing.backend.global.annotation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = {EnumValidator.class}) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Enum { + String message() default "enum 형식메 맞지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; + Class> enumClass(); + boolean ignoreCase() default false; +} diff --git a/src/main/java/com/moing/backend/global/annotation/EnumValidator.java b/src/main/java/com/moing/backend/global/annotation/EnumValidator.java new file mode 100644 index 00000000..ff82b057 --- /dev/null +++ b/src/main/java/com/moing/backend/global/annotation/EnumValidator.java @@ -0,0 +1,31 @@ +package com.moing.backend.global.annotation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class EnumValidator implements ConstraintValidator { + + private Enum annotation; + + @Override + public void initialize(Enum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + boolean result = false; + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value.equals(enumValue.toString()) + || (this.annotation.ignoreCase() && value.equalsIgnoreCase(enumValue.toString()))) { + result = true; + break; + } + } + } + return result; + } + +} diff --git a/src/main/java/com/moing/backend/global/annotation/Mapper.java b/src/main/java/com/moing/backend/global/annotation/Mapper.java new file mode 100644 index 00000000..dfd4d442 --- /dev/null +++ b/src/main/java/com/moing/backend/global/annotation/Mapper.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Mapper { +} diff --git a/src/main/java/com/moing/backend/global/annotation/Util.java b/src/main/java/com/moing/backend/global/annotation/Util.java new file mode 100644 index 00000000..20eb01b1 --- /dev/null +++ b/src/main/java/com/moing/backend/global/annotation/Util.java @@ -0,0 +1,15 @@ +package com.moing.backend.global.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Util { + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/src/main/java/com/moing/backend/global/annotation/ValidEnum.java b/src/main/java/com/moing/backend/global/annotation/ValidEnum.java new file mode 100644 index 00000000..a1c53508 --- /dev/null +++ b/src/main/java/com/moing/backend/global/annotation/ValidEnum.java @@ -0,0 +1,21 @@ +package com.moing.backend.global.annotation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = {EnumValidator.class}) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidEnum { + String message() default "Invalid value. This is not permitted."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + Class> enumClass(); +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/fcm/FcmConfig.java b/src/main/java/com/moing/backend/global/config/fcm/FcmConfig.java new file mode 100644 index 00000000..cf375005 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/FcmConfig.java @@ -0,0 +1,62 @@ +package com.moing.backend.global.config.fcm; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import com.moing.backend.global.config.fcm.exception.InitializeException; +import com.moing.backend.global.config.fcm.exception.MessagingException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +@Configuration +@Slf4j +public class FcmConfig { + + @Value("${firebase.config.path}") + private String firebaseConfigPath; + + @Value("${firebase.config.projectId}") + private String projectId; + + @Bean + public FirebaseApp firebaseApp() { + try { + // FileInputStream 대신 ClassPathResource를 사용하여 파일 로드 + ClassPathResource resource = new ClassPathResource(firebaseConfigPath); + InputStream serviceAccount = resource.getInputStream(); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .setProjectId(projectId) + .build(); + + return FirebaseApp.initializeApp(options); + } catch (FileNotFoundException e) { + throw new IllegalStateException("파일을 찾을 수 없습니다." + e.getMessage()); + } catch (IOException e) { + throw new InitializeException(); + } + } + + @Bean + public FirebaseMessaging firebaseMessaging() { + try { + return FirebaseMessaging.getInstance(firebaseApp()); + } catch (IllegalStateException e) { + throw new MessagingException("FirebaseApp 초기화에 실패하였습니다." + e.getMessage()); + } catch (NullPointerException e) { + throw new IllegalStateException("FirebaseApp을 불러오는데 실패하였습니다." + e.getMessage()); + } catch (Exception e) { + throw new IllegalArgumentException("firebaseConfigPath를 읽어오는데 실패하였습니다." + e.getMessage()); + } + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/ApproveTeamMessage.java b/src/main/java/com/moing/backend/global/config/fcm/constant/ApproveTeamMessage.java new file mode 100644 index 00000000..f5a6a3d8 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/ApproveTeamMessage.java @@ -0,0 +1,22 @@ +package com.moing.backend.global.config.fcm.constant; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ApproveTeamMessage { + + APPROVE_TEAM_MESSAGE("%s님, [%s] 모임이 타오를 준비를 마쳤어요!", "지금 바로 우리 모임원들을 초대해볼까요? 🔥"), + + REJECT_TEAM_MESSAGE("%s님, [%s] 모임 신청이 반려됐어요.", "신청서를 점검한 뒤 다시 한번 모임을 신청해주세요!"); + + private final String title; + private final String body; + + public String title(String memberName, String teamName) { + return String.format(title, memberName, teamName); + } + + public String body() { + return body; + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/FireThrowMessage.java b/src/main/java/com/moing/backend/global/config/fcm/constant/FireThrowMessage.java new file mode 100644 index 00000000..ef720ec8 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/FireThrowMessage.java @@ -0,0 +1,33 @@ +package com.moing.backend.global.config.fcm.constant; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FireThrowMessage { + + NEW_FIRE_THROW_TITLE1("어라… 왜 이렇게 발등이 뜨겁지?\uD83E\uDD28"), + NEW_FIRE_THROW_TITLE2("⚠\uFE0F불조심⚠\uFE0F "), + NEW_FIRE_THROW_TITLE_WITH_COMMENT("%s님이 불을 던졌어요!"), + + + NEW_FIRE_THROW_MESSAGE1("%s님이 %s님에게 불을 던졌어요! 어서 미션을 인증해볼까요?"), + NEW_FIRE_THROW_MESSAGE2("%s님! %s님이 던진 불에 타버릴지도 몰라요! 어서 인증하러갈까요?"); + + + private final String message; + + public String to(String pusher) { + return String.format(message, pusher); + } + public String fromTo(String pusher,String receiver) { + return String.format(message, pusher,receiver); + } + public String toFrom(String receiver,String pusher) { + return String.format(message, receiver,pusher); + } + + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/NewCommentUploadMessage.java b/src/main/java/com/moing/backend/global/config/fcm/constant/NewCommentUploadMessage.java new file mode 100644 index 00000000..a71edac9 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/NewCommentUploadMessage.java @@ -0,0 +1,23 @@ +package com.moing.backend.global.config.fcm.constant; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NewCommentUploadMessage { + + NEW_COMMENT_UPLOAD_MESSAGE("%s", "[%s님의 댓글] %s"); + + private final String title; + private final String body; + + public String title(String commentContent) { + return String.format(title, commentContent); + } + + public String body(String writerNickname, String boardTitle) { + return String.format(body, writerNickname, boardTitle); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/NewMissionTitle.java b/src/main/java/com/moing/backend/global/config/fcm/constant/NewMissionTitle.java new file mode 100644 index 00000000..9bb30c20 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/NewMissionTitle.java @@ -0,0 +1,13 @@ +package com.moing.backend.global.config.fcm.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NewMissionTitle { + + NEW_SINGLE_MISSION_COMING("의 새로운 미션이 등장했어요! "), + NEW_REPEAT_MISSION_COMING("의 새로운 반복미션이 시작되었어요! "); + private final String title; +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/NewNoticeUploadMessage.java b/src/main/java/com/moing/backend/global/config/fcm/constant/NewNoticeUploadMessage.java new file mode 100644 index 00000000..76f6b270 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/NewNoticeUploadMessage.java @@ -0,0 +1,22 @@ +package com.moing.backend.global.config.fcm.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NewNoticeUploadMessage { + + NEW_NOTICE_UPLOAD_MESSAGE("%s에 새로 올라온 공지를 확인하세요!", "%s"); + + private final String title; + private final String body; + + public String title(String teamName) { + return String.format(title, teamName); + } + + public String body(String noticeTitle) { + return String.format(body, noticeTitle); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/NewUploadTitle.java b/src/main/java/com/moing/backend/global/config/fcm/constant/NewUploadTitle.java new file mode 100644 index 00000000..98690142 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/NewUploadTitle.java @@ -0,0 +1,13 @@ +package com.moing.backend.global.config.fcm.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NewUploadTitle{ + UPLOAD_NOTICE_NEW_TITLE("새로운 공지 알려드려요!"), + UPLOAD_VOTE_NEW_TITLE("새로운 투표 알려드려요!"), + UPLOAD_MISSION_NEW_TITLE("새로운 미션 알려드려요!"); + private final String title; +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/constant/RemindMissionTitle.java b/src/main/java/com/moing/backend/global/config/fcm/constant/RemindMissionTitle.java new file mode 100644 index 00000000..95ee96f4 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/constant/RemindMissionTitle.java @@ -0,0 +1,32 @@ +package com.moing.backend.global.config.fcm.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum RemindMissionTitle { + + REMIND_MISSION_TITLE1("오늘도 좋은 하루 보내세요!"), + REMIND_MISSION_TITLE2("혹시 ... 잊으신 건 아니죠?\uD83D\uDC40"), + REMIND_MISSION_TITLE3("아직 인증하지 않은 미션이 있어요!"), + REMIND_MISSION_TITLE4("오늘의 열정이 타오르불\uD83D\uDD25"), + + + REMIND_MISSION_MESSAGE1("자기계발 미션 도전과 함께⚡\uFE0F "), + REMIND_MISSION_MESSAGE2("미션 인증을 모임원들이 기다리고 있어요!"), + REMIND_MISSION_MESSAGE3("미션 인증하고 이번주도 도전해요👊"), + REMIND_MISSION_MESSAGE4("미션 도전하고 성취감 뿜뿜💪"), + + + REMIND_ON_SUNDAY_TITLE("이번 한 주도 고생 많았어요\uD83D\uDE0A"), + REMIND_ON_SUNDAY_MESSAGE("내일부터 미션을 다시 시작해봐요\uD83D\uDCAA"), + + + REMIND_ON_MONDAY_TITLE("이번주의 도전을 응원할게요\uD83D\uDC4A"), + REMIND_ON_MONDAY_MESSAGE("모잉과 함께 달려볼까요?"); + + + private final String message; + + } diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/event/MultiFcmEvent.java b/src/main/java/com/moing/backend/global/config/fcm/dto/event/MultiFcmEvent.java new file mode 100644 index 00000000..f944e40c --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/event/MultiFcmEvent.java @@ -0,0 +1,24 @@ +package com.moing.backend.global.config.fcm.dto.event; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.Optional; + +@Getter +@AllArgsConstructor +public class MultiFcmEvent { + + private String title; + private String body; + private Optional> idAndTokensByPush; + private Optional> idAndTokensBySave; + private String idInfo; + private String name; + private AlarmType alarmType; + private String path; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/event/SingleFcmEvent.java b/src/main/java/com/moing/backend/global/config/fcm/dto/event/SingleFcmEvent.java new file mode 100644 index 00000000..290f7e74 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/event/SingleFcmEvent.java @@ -0,0 +1,24 @@ +package com.moing.backend.global.config.fcm.dto.event; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import com.moing.backend.domain.member.domain.entity.Member; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class SingleFcmEvent { + + private Member member; + private String title; + private String body; + private String idInfo; + private String name; + private AlarmType alarmType; + private String path; + private boolean isAlarmPush; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/request/MultiRequest.java b/src/main/java/com/moing/backend/global/config/fcm/dto/request/MultiRequest.java new file mode 100644 index 00000000..2a0cdfed --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/request/MultiRequest.java @@ -0,0 +1,32 @@ +package com.moing.backend.global.config.fcm.dto.request; + +import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MultiRequest { + + private List memberIdAndTokens; + + private String title; + + private String body; + + private String idInfo; + + private String name; + + private AlarmType alarmType; + + private String path; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/request/SingleRequest.java b/src/main/java/com/moing/backend/global/config/fcm/dto/request/SingleRequest.java new file mode 100644 index 00000000..a27a067a --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/request/SingleRequest.java @@ -0,0 +1,29 @@ +package com.moing.backend.global.config.fcm.dto.request; + +import com.moing.backend.domain.history.domain.entity.AlarmType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SingleRequest { + + private String registrationToken; + + private String title; + + private String body; + + private Long memberId; + + private String idInfo; + + private String name; + + private AlarmType alarmType; + + private String path; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/response/MultiResponse.java b/src/main/java/com/moing/backend/global/config/fcm/dto/response/MultiResponse.java new file mode 100644 index 00000000..39f67767 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/response/MultiResponse.java @@ -0,0 +1,14 @@ +package com.moing.backend.global.config.fcm.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class MultiResponse { + private final String response; + private final List failedTokens; + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/fcm/dto/response/SingleResponse.java b/src/main/java/com/moing/backend/global/config/fcm/dto/response/SingleResponse.java new file mode 100644 index 00000000..b4f0cbf9 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/dto/response/SingleResponse.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SingleResponse { + private final String response; + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/ExceptionHandler.java b/src/main/java/com/moing/backend/global/config/fcm/exception/ExceptionHandler.java new file mode 100644 index 00000000..daca80d0 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/ExceptionHandler.java @@ -0,0 +1,26 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.google.firebase.messaging.FirebaseMessagingException; + +public class ExceptionHandler { + + public static NotificationException handleFirebaseMessagingException(FirebaseMessagingException e) { + String errorCode = e.getErrorCode().name(); + String errorMessage = e.getMessage(); + + switch (errorCode) { + case "INVALID_ARGUMENT": + return new NotificationException("올바르지 않은 인자 값입니다: " + errorMessage); + case "NOT_FOUND": + return new NotificationException("등록 토큰이 유효하지 않거나, 주제(Topic)가 존재하지 않습니다: " + errorMessage); + case "UNREGISTERED": + return new NotificationException("해당 주제(Topic)의 구독이 해지되었습니다: " + errorMessage); + case "UNAVAILABLE": + return new NotificationException("서비스를 사용할 수 없습니다: " + errorMessage); + default: + e.printStackTrace(); + return new NotificationException("메시지 전송에 실패했습니다: " + errorMessage); + } + } +} + diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/FirebaseException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/FirebaseException.java new file mode 100644 index 00000000..11c67de3 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/FirebaseException.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class FirebaseException extends ApplicationException { + protected FirebaseException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode, httpStatus); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/InitializeException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/InitializeException.java new file mode 100644 index 00000000..34b560ca --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/InitializeException.java @@ -0,0 +1,12 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class InitializeException extends FirebaseException { + public InitializeException() { + super(ErrorCode.INITIALIZE_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/MessagingException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/MessagingException.java new file mode 100644 index 00000000..0acc476b --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/MessagingException.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class MessagingException extends FirebaseException { + public MessagingException(String message) { + super(ErrorCode.MESSAGING_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/exception/NotificationException.java b/src/main/java/com/moing/backend/global/config/fcm/exception/NotificationException.java new file mode 100644 index 00000000..b7dd8a90 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/exception/NotificationException.java @@ -0,0 +1,11 @@ +package com.moing.backend.global.config.fcm.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotificationException extends FirebaseException { + public NotificationException(String message) { + super(ErrorCode.NOTIFICATION_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/service/MessageSender.java b/src/main/java/com/moing/backend/global/config/fcm/service/MessageSender.java new file mode 100644 index 00000000..d6b73aeb --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/service/MessageSender.java @@ -0,0 +1,5 @@ +package com.moing.backend.global.config.fcm.service; + +public interface MessageSender { + void send(T request); +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/service/MultiMessageSender.java b/src/main/java/com/moing/backend/global/config/fcm/service/MultiMessageSender.java new file mode 100644 index 00000000..f5bdcfba --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/service/MultiMessageSender.java @@ -0,0 +1,101 @@ +package com.moing.backend.global.config.fcm.service; + +import com.google.firebase.messaging.*; +import com.moing.backend.domain.history.application.mapper.AlarmHistoryMapper; +import com.moing.backend.global.config.fcm.dto.request.MultiRequest; +import com.moing.backend.global.config.fcm.dto.response.MultiResponse; +import com.moing.backend.global.config.fcm.exception.ExceptionHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class MultiMessageSender implements MessageSender { + + private final FirebaseMessaging firebaseMessaging; + + @Override + @Retryable(value = FirebaseMessagingException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000)) + public void send(MultiRequest request) { + + + List fcmTokens = AlarmHistoryMapper.getFcmTokens(request.getMemberIdAndTokens()); + Notification notification = Notification.builder() + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build(); + + // Android Configuration + AndroidConfig androidConfig = AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setNotification(AndroidNotification.builder() + .setChannelId("FCM_Channel") + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build()) + .build(); + + // APNs Configuration + ApnsConfig apnsConfig = ApnsConfig.builder() + .setAps(Aps.builder() + .setCategory("YOUR_CATEGORY") // Replace with your category + .setAlert(ApsAlert.builder() + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build()) + .build()) + .build(); + + Map additionalData = new HashMap<>(); + additionalData.put("path", request.getPath()); + additionalData.put("idInfo", request.getIdInfo()); + + MulticastMessage message = MulticastMessage.builder() + .addAllTokens(fcmTokens) + .setNotification(notification) + .setAndroidConfig(androidConfig) // Applying Android configuration + .setApnsConfig(apnsConfig) // Applying APNs configuration + .putAllData(additionalData) + .build(); + + try { +// BatchResponse response = firebaseMessaging.sendMulticast(message); + BatchResponse response = firebaseMessaging.sendEachForMulticast(message); + + + List failedTokens = new ArrayList<>(); + + if (response.getFailureCount() > 0) { + List responses = response.getResponses(); + + List memberIds = AlarmHistoryMapper.getMemberIds(request.getMemberIdAndTokens()); + + for (int i = 0; i < responses.size(); i++) { + if (!responses.get(i).isSuccessful()) { + // Add the failed tokens to the list + failedTokens.add(fcmTokens.get(i)); + } + } + } + + String messageString = String.format("%d messages were sent successfully.", response.getSuccessCount()); + + new MultiResponse(messageString, failedTokens); + + } catch (FirebaseMessagingException e) { + throw ExceptionHandler.handleFirebaseMessagingException(e); + } + } + +} diff --git a/src/main/java/com/moing/backend/global/config/fcm/service/SingleMessageSender.java b/src/main/java/com/moing/backend/global/config/fcm/service/SingleMessageSender.java new file mode 100644 index 00000000..fa069c8e --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/fcm/service/SingleMessageSender.java @@ -0,0 +1,73 @@ +package com.moing.backend.global.config.fcm.service; + +import com.google.firebase.messaging.*; +import com.moing.backend.global.config.fcm.dto.request.SingleRequest; +import com.moing.backend.global.config.fcm.dto.response.SingleResponse; +import com.moing.backend.global.config.fcm.exception.ExceptionHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.util.HashMap; +import java.util.Map; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class SingleMessageSender implements MessageSender { + + private final FirebaseMessaging firebaseMessaging; + + @Override + @Retryable(value = FirebaseMessagingException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000)) + public void send(SingleRequest request){ + Notification notification = Notification.builder() + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build(); + + // Android Configuration + AndroidConfig androidConfig = AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setNotification(AndroidNotification.builder() + .setChannelId("FCM_Channel") + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build()) + .build(); + + // APNs Configuration + ApnsConfig apnsConfig = ApnsConfig.builder() + .setAps(Aps.builder() + .setCategory("YOUR_CATEGORY") // Replace with your category + .setAlert(ApsAlert.builder() + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build()) + .build()) + .build(); + + Map additionalData = new HashMap<>(); + additionalData.put("path", request.getPath()); + additionalData.put("idInfo", request.getIdInfo()); + + Message message = Message.builder() + .setToken(request.getRegistrationToken()) + .setNotification(notification) + .setAndroidConfig(androidConfig) // Applying Android configuration + .setApnsConfig(apnsConfig) // Applying APNs configuration + .putAllData(additionalData) + .build(); + + try { + String response = firebaseMessaging.send(message); + new SingleResponse(response); + } catch (FirebaseMessagingException e) { + throw ExceptionHandler.handleFirebaseMessagingException(e); + } + } +} diff --git a/src/main/java/com/moing/backend/global/config/querydsl/QuerydslConfig.java b/src/main/java/com/moing/backend/global/config/querydsl/QuerydslConfig.java new file mode 100644 index 00000000..4d09a493 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/querydsl/QuerydslConfig.java @@ -0,0 +1,19 @@ +package com.moing.backend.global.config.querydsl; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@Configuration +public class QuerydslConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(this.entityManager); + } +} diff --git a/src/main/java/com/moing/backend/global/config/redis/RedisConfig.java b/src/main/java/com/moing/backend/global/config/redis/RedisConfig.java new file mode 100644 index 00000000..7b804c78 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/redis/RedisConfig.java @@ -0,0 +1,42 @@ +package com.moing.backend.global.config.redis; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.redis.host}") + private String redisHost; + + @Value("${spring.redis.port}") + private String redisPort; + + @Value("${spring.redis.password}") + private String redisPassword; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(redisHost); + redisStandaloneConfiguration.setPort(Integer.parseInt(redisPort)); + redisStandaloneConfiguration.setPassword(redisPassword); + LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration); + return lettuceConnectionFactory; + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} diff --git a/src/main/java/com/moing/backend/global/config/redis/RedisUtil.java b/src/main/java/com/moing/backend/global/config/redis/RedisUtil.java new file mode 100644 index 00000000..71bb8ee9 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/redis/RedisUtil.java @@ -0,0 +1,35 @@ +package com.moing.backend.global.config.redis; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Repository +public class RedisUtil { + + private RedisTemplate redisTemplate; + + @Value("${jwt.refresh-token-period}") + private long refreshTokenValidityTime; + + public RedisUtil(final RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void save(String refreshToken, String socialId) { + //동일한 key 값으로 저장하면 value값 updat됨 + redisTemplate.opsForValue().set(socialId, refreshToken, refreshTokenValidityTime/1000, TimeUnit.SECONDS); + } + + public void deleteById(String socialId) { + redisTemplate.delete(String.valueOf(socialId)); + } + + public Optional findById(final String socialId) { + String refreshToken = (String) redisTemplate.opsForValue().get(socialId); + return Optional.ofNullable(refreshToken); + } +} diff --git a/src/main/java/com/moing/backend/global/config/redis/constant/RefreshTokenConstant.java b/src/main/java/com/moing/backend/global/config/redis/constant/RefreshTokenConstant.java new file mode 100644 index 00000000..f8213e9c --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/redis/constant/RefreshTokenConstant.java @@ -0,0 +1,23 @@ +package com.moing.backend.global.config.redis.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +public class RefreshTokenConstant { + @Getter + @RequiredArgsConstructor + public enum ERefreshTokenMessage { + TOKEN_REFRESH_SUCCESS("토큰 재발급을 완료하였습니다"); + private final String message; + } + + @Getter + @RequiredArgsConstructor + public enum RefreshTokenExceptionList { + NOT_FOUND_TOKEN_ERROR("R0003", HttpStatus.NOT_FOUND, "Refresh-Token을 찾을 수 없습니다. 아예 새롭게 로그인을 하세요"); + private final String errorCode; + private final HttpStatus httpStatus; + private final String message; + } +} diff --git a/src/main/java/com/moing/backend/global/config/s3/ImageUrlUtil.java b/src/main/java/com/moing/backend/global/config/s3/ImageUrlUtil.java new file mode 100644 index 00000000..2e85b4fa --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/s3/ImageUrlUtil.java @@ -0,0 +1,14 @@ +package com.moing.backend.global.config.s3; + +import com.moing.backend.global.annotation.Util; +import org.springframework.beans.factory.annotation.Value; + +@Util +public class ImageUrlUtil { + public static String prefix; + + @Value("${image.prefix}") + public void setPrefix(String value) { + prefix = value; + } +} diff --git a/src/main/java/com/moing/backend/global/config/s3/S3Config.java b/src/main/java/com/moing/backend/global/config/s3/S3Config.java new file mode 100644 index 00000000..5b2078ae --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/s3/S3Config.java @@ -0,0 +1,33 @@ +package com.moing.backend.global.config.s3; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) + AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} diff --git a/src/main/java/com/moing/backend/global/config/s3/S3Service.java b/src/main/java/com/moing/backend/global/config/s3/S3Service.java new file mode 100644 index 00000000..9e55e7c1 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/s3/S3Service.java @@ -0,0 +1,72 @@ +package com.moing.backend.global.config.s3; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.Headers; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.moing.backend.domain.infra.image.application.dto.ImageFileExtension; +import com.moing.backend.domain.infra.image.application.dto.ImageUrlDto; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class S3Service { + private final AmazonS3Client amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + + public ImageUrlDto issuePreSignedUrl(ImageFileExtension fileExtension) { + + String valueFileExtension = fileExtension.getUploadExtension(); + + String fileName = createFileName(valueFileExtension); + + GeneratePresignedUrlRequest request = getGeneratePreSignedUrlRequest(bucket, fileName, valueFileExtension); + String url = amazonS3.generatePresignedUrl(request).toString(); + + return ImageUrlDto.of(url, fileName); + } + + public void deleteImage(String fileUrl) { + String splitStr = ".com/"; + String fileName = fileUrl.substring(fileUrl.lastIndexOf(splitStr) + splitStr.length()); + + if (amazonS3.doesObjectExist(bucket, fileName)) { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } + } + + private String createFileName(String fileExtension) { + return UUID.randomUUID() + "." + fileExtension; + } + + // 업로드용 Pre-Signed URL을 생성하기 때문에, PUT을 지정 + private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest( + String bucket, String fileName, String fileExtension) { + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(HttpMethod.PUT) + .withExpiration(getPreSignedUrlExpiration()); + generatePresignedUrlRequest.addRequestParameter( + Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); + return generatePresignedUrlRequest; + } + + private Date getPreSignedUrlExpiration() { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Seoul")); + calendar.add(Calendar.MINUTE, 5); // 5분 추가 + return calendar.getTime(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/security/SecurityConfig.java b/src/main/java/com/moing/backend/global/config/security/SecurityConfig.java new file mode 100644 index 00000000..76259ea4 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/SecurityConfig.java @@ -0,0 +1,74 @@ +package com.moing.backend.global.config.security; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.global.config.security.filter.JwtAccessDeniedHandler; +import com.moing.backend.global.config.security.filter.JwtAuthenticationEntryPoint; +import com.moing.backend.global.config.security.jwt.JwtSecurityConfig; +import com.moing.backend.global.config.security.jwt.TokenUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@RequiredArgsConstructor +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final TokenUtil tokenUtil; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final MemberGetService memberQueryService; + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/resource/**", "/css/**", "/js/**", "/img/**", "/lib/**"); + } + + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() //CSRF 보호 비활성화 + .formLogin().disable() //폼 로그인 비활성화 + .httpBasic().disable() // HTTP 기본 인증 비활성화 + .exceptionHandling() //예외 처리 설정 + .authenticationEntryPoint(jwtAuthenticationEntryPoint) //인증되지 않은 사용자가 보호된 리소스에 액세스 할 때 호출되는 JwtAuthenticationEntryPoint 설정 + .accessDeniedHandler(jwtAccessDeniedHandler) //권한이 없는 사용자가 보호된 리소스에 액세스 할 때 호출되는 JwtAccessDeniedHandler 설정 + .and() + .headers().frameOptions().sameOrigin() + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //Spring Security에서 세션을 사용하지 않도록 설정 + .and() + .authorizeRequests() + .antMatchers("/swagger-resources/**").permitAll() + .antMatchers("/swagger-ui/**").permitAll() + .antMatchers("/webjars/**").permitAll() + .antMatchers("/v3/api-docs").permitAll() + .antMatchers("/api/auth/**", "/docs/**", "/api/image/**", "/actuator", "/actuator/*").permitAll() + .anyRequest().authenticated() + .and() + .apply(new JwtSecurityConfig(tokenUtil, memberQueryService)); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.addAllowedOriginPattern("*"); + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + configuration.setAllowCredentials(false); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/security/dto/User.java b/src/main/java/com/moing/backend/global/config/security/dto/User.java new file mode 100644 index 00000000..6e3580d2 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/dto/User.java @@ -0,0 +1,19 @@ +package com.moing.backend.global.config.security.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class User { + private String socialId; + private String email; + private List roles; +} + diff --git a/src/main/java/com/moing/backend/global/config/security/filter/JwtAccessDeniedHandler.java b/src/main/java/com/moing/backend/global/config/security/filter/JwtAccessDeniedHandler.java new file mode 100644 index 00000000..65718db1 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/filter/JwtAccessDeniedHandler.java @@ -0,0 +1,26 @@ +package com.moing.backend.global.config.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moing.backend.global.response.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + ErrorResponse errorResponse = new ErrorResponse("403", "접근이 금지되었습니다. 권한이 없는 사용자가 접근하려고 했습니다."); + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getWriter(), errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/security/filter/JwtAuthFilter.java b/src/main/java/com/moing/backend/global/config/security/filter/JwtAuthFilter.java new file mode 100644 index 00000000..7b4f0642 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/filter/JwtAuthFilter.java @@ -0,0 +1,81 @@ +package com.moing.backend.global.config.security.filter; + +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.JwtExceptionList; +import com.moing.backend.global.config.security.jwt.TokenUtil; +import com.moing.backend.global.config.security.util.AuthenticationUtil; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + private final TokenUtil tokenUtil; + private final MemberGetService memberQueryService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, JwtException { + String token = resolveToken(request); + String requestURI = request.getRequestURI(); + try { + if (StringUtils.hasText(token) && tokenUtil.verifyToken(token)) { + boolean isAdditionalInfoProvided = tokenUtil.getAdditionalInfoProvided(token); + if (isAdditionalInfoProvided) { + // 토큰 파싱해서 socialId 정보 가져오기 + String socialId = tokenUtil.getSocialId(token); + Member member = memberQueryService.getMemberBySocialId(socialId); + + // 이메일로 Authentication 정보 생성 + AuthenticationUtil.makeAuthentication(member); + log.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", member.getSocialId(), requestURI); + } else { + request.setAttribute("exception", JwtExceptionList.ADDITIONAL_REQUIRED_TOKEN.getErrorCode()); + } + } + } catch (SecurityException | MalformedJwtException e) { + request.setAttribute("exception", JwtExceptionList.MAL_FORMED_TOKEN.getErrorCode()); + } catch (ExpiredJwtException e) { + request.setAttribute("exception", JwtExceptionList.EXPIRED_TOKEN.getErrorCode()); + log.info("JwtAuthFilter: Caught ExpiredJwtException"); + log.info(String.valueOf(request.getAttribute("exception"))); + } catch (UnsupportedJwtException e) { + request.setAttribute("exception", JwtExceptionList.UNSUPPORTED_TOKEN.getErrorCode()); + } catch (IllegalArgumentException e) { + request.setAttribute("exception", JwtExceptionList.ILLEGAL_TOKEN.getErrorCode()); + } catch (Exception e) { + log.error("================================================"); + log.error("JwtFilter - doFilterInternal() 오류발생"); + log.error("token : {}", token); + log.error("Exception Message : {}", e.getMessage()); + log.error("Exception StackTrace : {"); + e.printStackTrace(); + log.error("}"); + log.error("================================================"); + request.setAttribute("exception", JwtExceptionList.UNKNOWN_ERROR.getErrorCode()); + } + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/moing/backend/global/config/security/filter/JwtAuthenticationEntryPoint.java b/src/main/java/com/moing/backend/global/config/security/filter/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..eef927a8 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/filter/JwtAuthenticationEntryPoint.java @@ -0,0 +1,71 @@ +package com.moing.backend.global.config.security.filter; + +import com.moing.backend.global.config.security.jwt.JwtExceptionList; +import com.nimbusds.jose.shaded.json.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; + +/** + * 인증되지 않은 사용자가 보호된 리소스에 액세스 할 때 호출 + */ +@Component +@Slf4j +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + String exception = String.valueOf(request.getAttribute("exception")); + log.info("commence:" + exception); + + switch (exception) { + case "J0007": + setResponse(response, JwtExceptionList.ADDITIONAL_REQUIRED_TOKEN); + break; + + case "J0001": + setResponse(response, JwtExceptionList.UNKNOWN_ERROR); + break; + + case "J0002": + setResponse(response, JwtExceptionList.MAL_FORMED_TOKEN); + break; + + case "J0006": + setResponse(response, JwtExceptionList.ILLEGAL_TOKEN); + break; + + case "J0003": + setResponse(response, JwtExceptionList.EXPIRED_TOKEN); + break; + + case "J0004": + setResponse(response, JwtExceptionList.UNSUPPORTED_TOKEN); + break; + + default: + setResponse(response, JwtExceptionList.ACCESS_DENIED); + break; + } + + } + + private void setResponse(HttpServletResponse response, JwtExceptionList exceptionCode) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + JSONObject responseJson = new JSONObject(); + responseJson.put("isSuccess", false); + responseJson.put("timestamp", LocalDateTime.now().withNano(0).toString()); + responseJson.put("errorCode", exceptionCode.getErrorCode()); + responseJson.put("message", exceptionCode.getMessage()); + + response.getWriter().print(responseJson); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/security/jwt/JwtExceptionList.java b/src/main/java/com/moing/backend/global/config/security/jwt/JwtExceptionList.java new file mode 100644 index 00000000..a7052801 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/jwt/JwtExceptionList.java @@ -0,0 +1,21 @@ +package com.moing.backend.global.config.security.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum JwtExceptionList { + UNKNOWN_ERROR("J0001", HttpStatus.INTERNAL_SERVER_ERROR, "예상치 못한 오류가 발생했습니다."), + MAL_FORMED_TOKEN("J0002", HttpStatus.UNAUTHORIZED, "잘못된 JWT 서명입니다."), + EXPIRED_TOKEN("J0003", HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), + UNSUPPORTED_TOKEN("J0004", HttpStatus.UNAUTHORIZED, "지원되지 않는 토큰입니다."), + ACCESS_DENIED("J0005", HttpStatus.UNAUTHORIZED, "접근이 거부되었습니다."), + ILLEGAL_TOKEN("J0006", HttpStatus.UNAUTHORIZED, "JWT 토큰이 잘못되었습니다."), + ADDITIONAL_REQUIRED_TOKEN("J0007", HttpStatus.UNAUTHORIZED, "추가 정보를 입력해야 합니다."); + private final String errorCode; + private final HttpStatus httpStatus; + private final String message; + +} diff --git a/src/main/java/com/moing/backend/global/config/security/jwt/JwtSecurityConfig.java b/src/main/java/com/moing/backend/global/config/security/jwt/JwtSecurityConfig.java new file mode 100644 index 00000000..bca70ba3 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/jwt/JwtSecurityConfig.java @@ -0,0 +1,24 @@ +package com.moing.backend.global.config.security.jwt; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.global.config.security.filter.JwtAuthFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@RequiredArgsConstructor +public class JwtSecurityConfig extends SecurityConfigurerAdapter { + + private final TokenUtil tokenUtil; + + private final MemberGetService memberQueryService; + + @Override + public void configure(HttpSecurity http) { + JwtAuthFilter customFilter = new JwtAuthFilter(tokenUtil, memberQueryService); + //UsernamePasswordAuthenticationFilter 앞에 필터로 JwtFilter 추가 + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/com/moing/backend/global/config/security/jwt/NotFoundRefreshToken.java b/src/main/java/com/moing/backend/global/config/security/jwt/NotFoundRefreshToken.java new file mode 100644 index 00000000..4c64a82c --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/jwt/NotFoundRefreshToken.java @@ -0,0 +1,13 @@ +package com.moing.backend.global.config.security.jwt; + +import com.moing.backend.global.exception.ApplicationException; +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public class NotFoundRefreshToken extends ApplicationException { + public NotFoundRefreshToken() { + super(ErrorCode.NOT_FOUND_REFRESH_TOKEN_ERROR, + HttpStatus.UNAUTHORIZED); + } + +} diff --git a/src/main/java/com/moing/backend/global/config/security/jwt/TokenUtil.java b/src/main/java/com/moing/backend/global/config/security/jwt/TokenUtil.java new file mode 100644 index 00000000..6b524846 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/jwt/TokenUtil.java @@ -0,0 +1,161 @@ +package com.moing.backend.global.config.security.jwt; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.global.config.redis.RedisUtil; +import com.moing.backend.global.response.TokenInfoResponse; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SecurityException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TokenUtil implements InitializingBean { + + private static final String ADDITIONAL_INFO = "isAdditionalInfoProvided"; + private final RedisUtil redisUtil; + private final MemberGetService memberQueryService; + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.access-token-period}") + private long accessTokenValidityTime; + + @Value("${jwt.refresh-token-period}") + private long refreshTokenValidityTime; + + private Key key; + + @Override + public void afterPropertiesSet() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + + /** + * 토큰 만드는 함수 + * + * @param member + * @return TokenInfoResponse + */ + public TokenInfoResponse createToken(Member member, boolean isAdditionalInfoProvided) { + // claim 생성 + Claims claims = getClaims(member, isAdditionalInfoProvided); + + Date now = new Date(); + Date accessTokenValidity = new Date(now.getTime() + this.accessTokenValidityTime); + Date refreshTokenValidity = new Date(now.getTime() + this.refreshTokenValidityTime); + + String accessToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(accessTokenValidity) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + + String refreshToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(refreshTokenValidity) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + + return TokenInfoResponse.from("Bearer", accessToken, refreshToken, refreshTokenValidityTime); + } + + public boolean verifyToken(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return claims.getBody().getExpiration().after(new Date()); + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("잘못된 JWT 서명입니다."); + throw e; + } catch (ExpiredJwtException e) { + log.info("만료된 JWT 토큰입니다."); + throw e; + } catch (UnsupportedJwtException e) { + log.info("지원되지 않는 JWT 토큰입니다."); + throw e; + } catch (IllegalArgumentException e) { + log.info("JWT 토큰이 잘못되었습니다."); + throw e; + } catch (Exception e) { + log.info(e.getMessage()); + throw e; + } + } + + public boolean verifyRefreshToken(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + //refresh token 관련 + public void storeRefreshToken(String socialId, TokenInfoResponse token) { + redisUtil.save(token.getRefreshToken(), socialId); + } + + public TokenInfoResponse tokenReissue(String token) { + + String socialId = getSocialId(token); + Member member = memberQueryService.getMemberBySocialId(socialId); + String storedRefreshToken = redisUtil.findById(socialId).orElseThrow(NotFoundRefreshToken::new); + + if(storedRefreshToken == null || !storedRefreshToken.equals(token)) { + throw new NotFoundRefreshToken(); + } + + // Token 생성 + TokenInfoResponse newToken = createToken(member, true); + + // Token 저장 + storeRefreshToken(socialId, newToken); + + return newToken; + } + + + //토큰 만료시키기 + public void expireRefreshToken(String socialId) { + redisUtil.deleteById(socialId); + } + + // get 함수 + public boolean getAdditionalInfoProvided(String token) { + Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); + return claims.get(ADDITIONAL_INFO, Boolean.class); + } + + private static Claims getClaims(Member member, boolean isAdditionalInfoProvided) { + // claim 에 socialId 정보 추가 + Claims claims = Jwts.claims().setSubject(member.getSocialId()); + claims.put(ADDITIONAL_INFO, isAdditionalInfoProvided); + return claims; + } + + private Date getExpiration(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getExpiration(); + } + + public String getSocialId(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + +} + diff --git a/src/main/java/com/moing/backend/global/config/security/oauth/CustomOAuth2UserService.java b/src/main/java/com/moing/backend/global/config/security/oauth/CustomOAuth2UserService.java new file mode 100644 index 00000000..94cda61a --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/oauth/CustomOAuth2UserService.java @@ -0,0 +1,44 @@ +package com.moing.backend.global.config.security.oauth; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; + +@Slf4j +@Service +public class CustomOAuth2UserService implements OAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); + + // 유저 정보 얻어오기 + String provider = userRequest.getClientRegistration().getRegistrationId(); + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + // 로그인 종류(Google, Kakao, Naver)에 맞게 OAuth2Attribute 생성 + OAuth2Attribute oAuth2Attribute = + OAuth2Attribute.of(provider, userNameAttributeName, oAuth2User.getAttributes()); + + log.info("{} : {}", provider, oAuth2Attribute); + + Map memberAttribute = oAuth2Attribute.convertToMap(); + + // 인증 정보 생성 후 반환 + return new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), + memberAttribute, "email"); + } +} + diff --git a/src/main/java/com/moing/backend/global/config/security/oauth/CustomUserDetails.java b/src/main/java/com/moing/backend/global/config/security/oauth/CustomUserDetails.java new file mode 100644 index 00000000..0123fb99 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/oauth/CustomUserDetails.java @@ -0,0 +1,58 @@ +package com.moing.backend.global.config.security.oauth; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import com.moing.backend.domain.member.domain.entity.Member; + +import java.util.Arrays; +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + + private Member member; + + public CustomUserDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + return Arrays.asList(new SimpleGrantedAuthority(member.getRole().name())); + } + + @Override + public String getPassword() { + // 비밀번호는 소셜 로그인에 사용되지 않으므로 null을 반환합니다. + return null; + } + + @Override + public String getUsername() { + return member.getNickName(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + public Member getMember() { + return member; + } +} diff --git a/src/main/java/com/moing/backend/global/config/security/oauth/CustomUserDetailsService.java b/src/main/java/com/moing/backend/global/config/security/oauth/CustomUserDetailsService.java new file mode 100644 index 00000000..c48027a9 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/oauth/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package com.moing.backend.global.config.security.oauth; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final MemberGetService memberGetService; + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String socialId) throws UsernameNotFoundException { + Member member = this.memberGetService.getMemberBySocialId(socialId); + + if (member == null) { + throw new UsernameNotFoundException("User not found with socialId: " + socialId); + } + + return new CustomUserDetails(member); + } +} + diff --git a/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2Attribute.java b/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2Attribute.java new file mode 100644 index 00000000..0c5ba658 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2Attribute.java @@ -0,0 +1,147 @@ +package com.moing.backend.global.config.security.oauth; + +import com.moing.backend.domain.member.domain.constant.SocialProvider; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.HashMap; +import java.util.Map; + +@ToString +@Builder(access = AccessLevel.PRIVATE) +@Getter +class OAuth2Attribute { + private Map attributes; + + private String socialId; + private SocialProvider provider; + private String name; + private String nickname; + private String email; + private String ageRange; + private String gender; + + + static OAuth2Attribute of(String provider, String attributeKey, + Map attributes) { + switch (provider) { + case "google": + return ofGoogle(attributeKey, attributes); + case "kakao": + return ofKakao("id", attributes); + case "apple": + return ofApple("id", attributes); + default: + throw new RuntimeException(); + } + } + + private static OAuth2Attribute ofGoogle(String attributeKey, + Map attributes) { + OAuth2Attribute oAuth2Attribute = OAuth2Attribute.builder() + .socialId(SocialProvider.GOOGLE + "@" + attributes.get(attributeKey)) + .provider(SocialProvider.GOOGLE) + .name((String) attributes.get("name")) + .nickname((String) attributes.get("name")) + .email((String) attributes.get("email")) + .ageRange("undef") + .gender("undef") + .build(); + + oAuth2Attribute.adaptGoogleResponse(); + + return oAuth2Attribute; + } + + private static OAuth2Attribute ofKakao(String attributeKey, + Map attributes) { + Map kakaoAccount = (Map) attributes.get("kakao_account"); + Map profile = (Map) kakaoAccount.get("profile"); + + OAuth2Attribute oAuth2Attribute = OAuth2Attribute.builder() + .socialId(SocialProvider.APPLE + "@" + attributes.get(attributeKey)) + .provider(SocialProvider.APPLE) + .name((String) profile.get("nickname")) + .nickname((String) profile.get("nickname")) + .email((String) kakaoAccount.get("email")) + .gender((String) kakaoAccount.get("gender")) + .ageRange((String) kakaoAccount.get("age_range")) + .build(); + + oAuth2Attribute.adaptKakaoResponse(); + + return oAuth2Attribute; + } + + private static OAuth2Attribute ofApple(String attributeKey, + Map attributes) { + OAuth2Attribute oAuth2Attribute = OAuth2Attribute.builder() + .socialId(SocialProvider.APPLE + "@" + attributes.get(attributeKey)) + .provider(SocialProvider.APPLE) + .name((String) attributes.get("email")) + .nickname("undef") + .email((String) attributes.get("email")) + .gender("undef") + .ageRange("undef") + .build(); + + oAuth2Attribute.adaptAppleResponse(); + + return oAuth2Attribute; + } + + Map convertToMap() { + Map map = new HashMap<>(); + map.put("socialId", socialId); + map.put("provider", provider); + map.put("name", name); + map.put("nickname", nickname); + map.put("email", email); + map.put("gender", gender); + map.put("age", ageRange); + + return map; + } + + public void adaptNaverResponse() { + if (gender == null || gender.isBlank()) gender = "undef"; + else if (gender.equals("M")) gender = "male"; + else if (gender.equals("F")) gender = "female"; + else gender = "undef"; + + if (ageRange == null || ageRange.isBlank()) ageRange = "undef"; + if (email.length() > 50) email = email.substring(0, 50); + if (nickname.length() > 7) { + name = name.substring(0, 7); + nickname = nickname.substring(0, 7); + } + } + + public void adaptKakaoResponse() { + if (gender == null || gender.isBlank()) gender = "undef"; + if (ageRange == null || ageRange.isBlank()) ageRange = "undef"; + if (email.length() > 50) email = email.substring(0, 50); + if (nickname.length() > 7) { + name = name.substring(0, 7); + nickname = nickname.substring(0, 7); + } + } + + public void adaptGoogleResponse() { + if (email.length() > 50) email = email.substring(0, 50); + if (name.length() > 7) { + name = name.substring(0, 7); + nickname = nickname.substring(0, 7); + } + } + + public void adaptAppleResponse() { + if (email.length() > 50) email = email.substring(0, 50); + if (name.length() > 7) { + name = name.substring(0, 7); + nickname = nickname.substring(0, 7); + } + } +} diff --git a/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2FailureHandler.java b/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2FailureHandler.java new file mode 100644 index 00000000..3578e1d0 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2FailureHandler.java @@ -0,0 +1,38 @@ +package com.moing.backend.global.config.security.oauth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moing.backend.global.response.ErrorCode; +import com.moing.backend.global.response.ErrorResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2FailureHandler implements AuthenticationFailureHandler { + + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { + log.error("[" + e.getClass().getSimpleName() + "] " + e.getMessage()); + + setErrorResponse(response, ErrorCode.INTERNAL_SERVER_ERROR); + } + + private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException { + response.setStatus(500); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), new ErrorResponse<>(errorCode)); + } +} diff --git a/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2SuccessHandler.java b/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2SuccessHandler.java new file mode 100644 index 00000000..c5fc1130 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/oauth/OAuth2SuccessHandler.java @@ -0,0 +1,62 @@ +package com.moing.backend.global.config.security.oauth; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.MemberSaveService; +import com.moing.backend.global.config.security.jwt.TokenUtil; +import com.moing.backend.global.response.TokenInfoResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +@Component +public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { + + private final MemberSaveService memberSaveService; + private final TokenUtil tokenUtil; + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + Member member = Member.valueOf(oAuth2User); + + // 회원가입(가입 정보 없는 유저일 때만) 및 로그인 + Member signInMember = memberSaveService.saveMember(member); + + // 토큰 생성 + TokenInfoResponse token = tokenUtil.createToken(signInMember, signInMember.getRegistrationStatus().equals(RegistrationStatus.COMPLETED)); + log.info("{}", token); + + // Redis에 Refresh Token 저장 + tokenUtil.storeRefreshToken(signInMember.getSocialId(), token); + + // Response message 생성 + writeOauthResponse(response, SignInResponse.from(token, signInMember.getRegistrationStatus())); + } + + private void writeOauthResponse(HttpServletResponse response, SignInResponse signInResponse) + throws IOException { + + response.setContentType("application/json;charset=UTF-8"); + + // Httpbody에 json 형태로 로그인 내용 추가 + var writer = response.getWriter(); + writer.println(objectMapper.writeValueAsString(signInResponse)); + writer.flush(); + } + +} + diff --git a/src/main/java/com/moing/backend/global/config/security/util/AuthenticationUtil.java b/src/main/java/com/moing/backend/global/config/security/util/AuthenticationUtil.java new file mode 100644 index 00000000..bc7f91c2 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/security/util/AuthenticationUtil.java @@ -0,0 +1,56 @@ +package com.moing.backend.global.config.security.util; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.global.config.security.dto.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class AuthenticationUtil { + + + public static String getCurrentUserEmail() { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return user.getEmail(); + } + + public static String getCurrentUserSocialId() { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return user.getSocialId(); + } + + public static Authentication getAuthentication(User user) { + + List grantedAuthorities = user.getRoles().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + return new UsernamePasswordAuthenticationToken(user, "", + grantedAuthorities); + } + + public static void makeAuthentication(Member member) { + // Authentication 정보 만들기 + User user = User.builder() + .socialId(member.getSocialId()) + .email(member.getEmail()) + .roles(Arrays.asList(member.getRole().getKey())) + .build(); + + // ContextHolder 에 Authentication 정보 저장 + Authentication auth = AuthenticationUtil.getAuthentication(user); + SecurityContextHolder.getContext().setAuthentication(auth); + } +} + + diff --git a/src/main/java/com/moing/backend/global/config/slack/SlackConfig.java b/src/main/java/com/moing/backend/global/config/slack/SlackConfig.java new file mode 100644 index 00000000..9f3dbe0d --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/SlackConfig.java @@ -0,0 +1,14 @@ +package com.moing.backend.global.config.slack; + +import com.slack.api.Slack; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SlackConfig { + + @Bean + public Slack slackClient() { + return Slack.getInstance(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java b/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java new file mode 100644 index 00000000..857523f8 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/exception/ExceptionEventHandler.java @@ -0,0 +1,23 @@ +package com.moing.backend.global.config.slack.exception; + +import com.moing.backend.global.config.slack.exception.dto.ExceptionEvent; +import com.moing.backend.global.config.slack.util.WebhookUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Profile("prod") +public class ExceptionEventHandler { + + private final WebhookUtil webhookUtil; + + @Async + @EventListener + public void onExceptionEvent(ExceptionEvent event) { + webhookUtil.sendSlackAlertErrorLog(event.getRequest(), event.getException()); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/slack/exception/dto/ExceptionEvent.java b/src/main/java/com/moing/backend/global/config/slack/exception/dto/ExceptionEvent.java new file mode 100644 index 00000000..b044d749 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/exception/dto/ExceptionEvent.java @@ -0,0 +1,14 @@ +package com.moing.backend.global.config.slack.exception.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.servlet.http.HttpServletRequest; + +@Getter +@AllArgsConstructor +public class ExceptionEvent { + + private final HttpServletRequest request; + private final Exception exception; +} diff --git a/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java b/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java new file mode 100644 index 00000000..0481017d --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/team/TeamCreateHandler.java @@ -0,0 +1,23 @@ +package com.moing.backend.global.config.slack.team; + +import com.moing.backend.global.config.slack.team.dto.TeamCreateEvent; +import com.moing.backend.global.config.slack.util.WebhookUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Profile("prod") +public class TeamCreateHandler { + + private final WebhookUtil webhookUtil; + + @Async + @EventListener + public void onTeamCreateEvent(TeamCreateEvent event) { + webhookUtil.sendSlackTeamCreatedMessage(event.getTeamName(), event.getLeaderId()); + } +} diff --git a/src/main/java/com/moing/backend/global/config/slack/team/dto/TeamCreateEvent.java b/src/main/java/com/moing/backend/global/config/slack/team/dto/TeamCreateEvent.java new file mode 100644 index 00000000..7a896a62 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/team/dto/TeamCreateEvent.java @@ -0,0 +1,13 @@ +package com.moing.backend.global.config.slack.team.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TeamCreateEvent { + + private final String teamName; + private final Long leaderId; + +} diff --git a/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java b/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java new file mode 100644 index 00000000..6272aeea --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/util/SlackAdapter.java @@ -0,0 +1,114 @@ +package com.moing.backend.global.config.slack.util; + +import com.slack.api.Slack; +import com.slack.api.model.Attachment; +import com.slack.api.model.Field; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.slack.api.webhook.WebhookPayloads.payload; + +@RequiredArgsConstructor +@Slf4j +@Component +public class SlackAdapter implements WebhookUtil { + + @Value("${webhook.slack.error_url}") + private String errorWebhookUrl; + + @Value("${webhook.slack.team_alarm_url}") + private String infoWebhookUrl; + + private final Slack slackClient = Slack.getInstance(); + + public void sendSlackMessage(String webhookUrl, String message, List attachments) { + try { + slackClient.send(webhookUrl, payload(p -> p + .text(message) + .attachments(attachments) + )); + } catch (IOException slackError) { + log.debug("Slack 통신과의 예외 발생"); + } + } + + @Override + public void sendSlackAlertErrorLog(HttpServletRequest request, Exception e) { + String message = "[500 에러가 발생했습니다.]"; + List attachments = List.of(generateSlackErrorAttachment(e, request)); + sendSlackMessage(errorWebhookUrl, message, attachments); + } + + @Override + public void sendSlackTeamCreatedMessage(String teamName, Long leaderId) { + String message = String.format("[새로운 소모임 '%s'이(가) 생성되었습니다.]", teamName); + List attachments = List.of(generateSlackTeamAttachment(teamName, leaderId)); + sendSlackMessage(infoWebhookUrl, message, attachments); + } + + private Attachment generateSlackTeamAttachment(String teamName, Long leaderId) { + return Attachment.builder() + .color("36a64f") + .title("소모임 생성 알림") + .fields(List.of( + generateSlackField("소모임 이름", teamName), + generateSlackField("생성자 아이디", String.valueOf(leaderId)) + )) + .build(); + } + + @Override + public void sendDailyStatsMessage(Map todayStats, Map yesterdayStats) { + String message = "[일일 통계 알림]"; + List attachments = todayStats.keySet().stream() + .map(key -> generateDailyStatsAttachment(key, todayStats.get(key), yesterdayStats.getOrDefault(key, 0L))) + .collect(Collectors.toList()); + + sendSlackMessage(infoWebhookUrl, message, attachments); + } + + private Attachment generateDailyStatsAttachment(String title, long todayCount, long yesterdayCount) { + return Attachment.builder() + .color("1A66CC") // 색상 설정 + .title(title) + .fields(List.of( + generateSlackField("Today", String.valueOf(todayCount) + " 개"), + generateSlackField("Yesterday", String.valueOf(yesterdayCount) + " 개") + )) + .build(); + } + + // attachment 생성 메서드 + private Attachment generateSlackErrorAttachment(Exception e, HttpServletRequest request) { + String requestTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").format(LocalDateTime.now()); + String xffHeader = request.getHeader("X-FORWARDED-FOR"); + return Attachment.builder() + .color("ff0000") + .title(requestTime + " 발생 에러 로그") + .fields(List.of( + generateSlackField("Request IP", xffHeader == null ? request.getRemoteAddr() : xffHeader), + generateSlackField("Request URL", request.getMethod() + " " + request.getRequestURL()), + generateSlackField("Error Message", e.getMessage()) + ) + ) + .build(); + } + + private Field generateSlackField(String title, String value) { + return Field.builder() + .title(title) + .value(value) + .valueShortEnough(false) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java b/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java new file mode 100644 index 00000000..4f010e76 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/slack/util/WebhookUtil.java @@ -0,0 +1,13 @@ +package com.moing.backend.global.config.slack.util; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +public interface WebhookUtil { + + void sendSlackAlertErrorLog(HttpServletRequest request, Exception e); + + void sendSlackTeamCreatedMessage(String teamName, Long leaderId); + + void sendDailyStatsMessage(Map todayStats, Map yesterdayStats); +} diff --git a/src/main/java/com/moing/backend/global/config/sns/AppleConfig.java b/src/main/java/com/moing/backend/global/config/sns/AppleConfig.java new file mode 100644 index 00000000..c1ad050b --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/sns/AppleConfig.java @@ -0,0 +1,42 @@ +package com.moing.backend.global.config.sns; + +import com.moing.backend.global.config.fcm.exception.InitializeException; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.FileCopyUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.security.PrivateKey; + +@Configuration +public class AppleConfig { + + + @Value("${oauth2.apple.keyPath}") + private String keyPath; + + @Bean + public PrivateKey applePrivateKey(){ + try{ + ClassPathResource resource = new ClassPathResource(keyPath); + String privateKeyString = new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); + Reader reader = new StringReader(privateKeyString); + PEMParser pemParser = new PEMParser(reader); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject(); + return converter.getPrivateKey(object); + } catch (FileNotFoundException e) { + throw new IllegalStateException("파일을 찾을 수 없습니다." + e.getMessage()); + } catch (IOException e) { + throw new InitializeException(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/config/webclient/WebClientConfig.java b/src/main/java/com/moing/backend/global/config/webclient/WebClientConfig.java new file mode 100644 index 00000000..06739876 --- /dev/null +++ b/src/main/java/com/moing/backend/global/config/webclient/WebClientConfig.java @@ -0,0 +1,77 @@ +package com.moing.backend.global.config.webclient; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.LoggingCodecSupport; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.tcp.TcpClient; + +import java.util.concurrent.TimeUnit; + +@Configuration +@Slf4j +public class WebClientConfig { + + @Bean + public WebClient webClient() { + ExchangeStrategies exchangeStrategies = configureExchangeStrategies(); + HttpClient httpClient = configureHttpClient(); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .exchangeStrategies(exchangeStrategies) + .filter(logRequest()) + .filter(logResponse()) + .defaultHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36") + .build(); + } + + private ExchangeStrategies configureExchangeStrategies() { + ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024 * 50)) + .build(); + enableLoggingRequestDetails(exchangeStrategies); + return exchangeStrategies; + } + + private void enableLoggingRequestDetails(ExchangeStrategies strategies) { + strategies.messageWriters().stream() + .filter(LoggingCodecSupport.class::isInstance) + .forEach(writer -> ((LoggingCodecSupport) writer).setEnableLoggingRequestDetails(true)); + } + + private HttpClient configureHttpClient() { + return HttpClient.from( + TcpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) + .doOnConnected(conn -> conn.addHandler(new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS))) + ); + } + + private ExchangeFilterFunction logRequest() { + return ExchangeFilterFunction.ofRequestProcessor( + clientRequest -> { + log.debug("Request: {} {}", clientRequest.method(), clientRequest.url()); + clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value))); + return Mono.just(clientRequest); + } + ); + } + + private ExchangeFilterFunction logResponse() { + return ExchangeFilterFunction.ofResponseProcessor( + clientResponse -> { + clientResponse.headers().asHttpHeaders().forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value))); + return Mono.just(clientResponse); + } + ); + } +} diff --git a/src/main/java/com/moing/backend/global/entity/BaseTimeEntity.java b/src/main/java/com/moing/backend/global/entity/BaseTimeEntity.java new file mode 100644 index 00000000..2278dde1 --- /dev/null +++ b/src/main/java/com/moing/backend/global/entity/BaseTimeEntity.java @@ -0,0 +1,24 @@ +package com.moing.backend.global.entity; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime lastModifiedDate; +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/exception/ApplicationException.java b/src/main/java/com/moing/backend/global/exception/ApplicationException.java new file mode 100644 index 00000000..c79eb581 --- /dev/null +++ b/src/main/java/com/moing/backend/global/exception/ApplicationException.java @@ -0,0 +1,23 @@ +package com.moing.backend.global.exception; + +import com.moing.backend.global.response.ErrorCode; +import org.springframework.http.HttpStatus; + +public abstract class ApplicationException extends RuntimeException { + + private final ErrorCode errorCode; + private final HttpStatus httpStatus; + + protected ApplicationException(ErrorCode errorCode, HttpStatus httpStatus) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.httpStatus = httpStatus; + } + + public ErrorCode getErrorCode() { return errorCode;} + + public HttpStatus getHttpStatus() { + return httpStatus; + } + +} diff --git a/src/main/java/com/moing/backend/global/exception/GlobalExceptionHandler.java b/src/main/java/com/moing/backend/global/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..d48461eb --- /dev/null +++ b/src/main/java/com/moing/backend/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,71 @@ +package com.moing.backend.global.exception; + +import com.moing.backend.global.config.slack.exception.dto.ExceptionEvent; +import com.moing.backend.global.response.ErrorCode; +import com.moing.backend.global.response.ErrorResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import java.util.function.Consumer; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; + +@RestControllerAdvice +@Slf4j +@RequiredArgsConstructor +public class GlobalExceptionHandler { + + private static final String LOG_FORMAT = "Class : {}, Code : {}, Message : {}"; + private final ApplicationEventPublisher eventPublisher; + + @ExceptionHandler(ApplicationException.class) + public ResponseEntity handleApplicationException(ApplicationException ex) { + return handleException(ex, ex.getErrorCode(), ex.getMessage(), ex.getHttpStatus(), log::warn); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity inputMethodArgumentInvalidExceptionHandler (MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + return handleException(ex, ErrorCode.BAD_REQUEST, message, HttpStatus.BAD_REQUEST, log::warn); + } + + @ExceptionHandler(PatternSyntaxException.class) + public ResponseEntity inputPatternSyntaxExceptionHandler(PatternSyntaxException ex) { + return handleException(ex, ErrorCode.BAD_REQUEST, ErrorCode.BAD_REQUEST.getMessage(), HttpStatus.BAD_REQUEST, log::warn); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity jsonParseExceptionHandler(HttpMessageNotReadableException ex) { + return handleException(ex, ErrorCode.BAD_REQUEST, ErrorCode.BAD_REQUEST.getMessage(), HttpStatus.BAD_REQUEST, log::warn); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity httpRequestNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + return handleException(ex, ErrorCode.METHOD_NOT_ALLOWED, ErrorCode.METHOD_NOT_ALLOWED.getMessage(), HttpStatus.METHOD_NOT_ALLOWED, log::warn); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity internalServerErrorHandler(Exception ex, HttpServletRequest request) { + eventPublisher.publishEvent(new ExceptionEvent(request, ex)); + return handleException(ex, ErrorCode.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, log::error); + } + + private ResponseEntity handleException(Exception ex, ErrorCode errorCode, String message, HttpStatus httpStatus, Consumer logger) { + log.error(LOG_FORMAT, ex.getClass().getSimpleName(), errorCode.getErrorCode(), ex.getMessage()); + ErrorResponse errorResponse = new ErrorResponse(errorCode, message); + return ResponseEntity.status(httpStatus.value()).body(errorResponse); + } + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/exception/InternalServerErrorException.java b/src/main/java/com/moing/backend/global/exception/InternalServerErrorException.java new file mode 100644 index 00000000..47916666 --- /dev/null +++ b/src/main/java/com/moing/backend/global/exception/InternalServerErrorException.java @@ -0,0 +1,6 @@ +package com.moing.backend.global.exception; + +public class InternalServerErrorException extends RuntimeException{ + + public InternalServerErrorException(String message){ super(message); } +} diff --git a/src/main/java/com/moing/backend/global/interceptor/AuthInterceptor.java b/src/main/java/com/moing/backend/global/interceptor/AuthInterceptor.java new file mode 100644 index 00000000..0e1d5df3 --- /dev/null +++ b/src/main/java/com/moing/backend/global/interceptor/AuthInterceptor.java @@ -0,0 +1,35 @@ +package com.moing.backend.global.interceptor; + +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.global.config.security.dto.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Slf4j +@RequiredArgsConstructor +@Component +public class AuthInterceptor implements HandlerInterceptor { + + private final MemberGetService memberQueryService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + User user = (User) authentication.getPrincipal(); + String socialId = user.getSocialId(); + + // socialId 를 갖는 사용자 존재 여부 + memberQueryService.getMemberBySocialId(socialId); + } + return true; + } +} + diff --git a/src/main/java/com/moing/backend/global/log/aop/LogAspect.java b/src/main/java/com/moing/backend/global/log/aop/LogAspect.java new file mode 100644 index 00000000..b7b6d636 --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/aop/LogAspect.java @@ -0,0 +1,42 @@ +package com.moing.backend.global.log.aop; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + + private final LogTrace logTrace; + + @Around("com.moing.backend.global.log.aop.Pointcuts.allService()") + public Object serviceLog(ProceedingJoinPoint joinPoint) throws Throwable { + return getObject(joinPoint); + } + + @Around("com.moing.backend.global.log.aop.Pointcuts.allController()") + public Object controllerLog(ProceedingJoinPoint joinPoint) throws Throwable { + return getObject(joinPoint); + } + + private Object getObject(ProceedingJoinPoint joinPoint) throws Throwable { + TraceStatus traceStatus = null; + try { + traceStatus = logTrace.start(joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName()); + Object result = joinPoint.proceed(); + logTrace.end(traceStatus); + return result; + }catch (Exception e) { + if (traceStatus != null) { + logTrace.exception(e, traceStatus); + } + throw e; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/log/aop/LogFormat.java b/src/main/java/com/moing/backend/global/log/aop/LogFormat.java new file mode 100644 index 00000000..9ca8ccc0 --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/aop/LogFormat.java @@ -0,0 +1,52 @@ +package com.moing.backend.global.log.aop; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.moing.backend.global.exception.ApplicationException; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LogFormat { + private final String threadId; + private final String className; + private final String methodName; + private final Long executeTime; + private final String errorCode; + private final String errorMessage; + private final Class errorClass; + private final StackTraceElement[] errorStackTrace; + + public static LogFormat createLogFormat(TraceStatus traceStatus) { + return LogFormat.builder() + .threadId(traceStatus.getThreadId()) + .className(traceStatus.getClassName()) + .methodName(traceStatus.getMethodName()) + .executeTime(System.currentTimeMillis() - traceStatus.getStartTime()) + .build(); + } + + public static LogFormat createErrorLogFormat(TraceStatus traceStatus, Exception exception) { + LogFormat.LogFormatBuilder logFormatBuilder = LogFormat.builder() + .threadId(traceStatus.getThreadId()) + .className(traceStatus.getClassName()) + .methodName(traceStatus.getMethodName()); + if (exception instanceof ApplicationException) { + ApplicationException ae = (ApplicationException) exception; + return logFormatBuilder + .errorCode(ae.getErrorCode().getErrorCode()) + .errorMessage(ae.getErrorCode().getMessage()) + .build(); + } else { + return logFormatBuilder + .errorClass(exception.getClass()) + .errorMessage(exception.getMessage()) + .errorStackTrace(exception.getStackTrace()) + .build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/log/aop/LogTrace.java b/src/main/java/com/moing/backend/global/log/aop/LogTrace.java new file mode 100644 index 00000000..e16b3653 --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/aop/LogTrace.java @@ -0,0 +1,64 @@ +package com.moing.backend.global.log.aop; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LogTrace { + + private static final ThreadLocal threadId = new ThreadLocal<>(); + private static final Integer WARN_REQUEST_TIME = 1000; + + private final ObjectMapper objectMapper; + + + public TraceStatus start(String fullClassName, String method) { + String id = threadId.get(); + long startTime = System.currentTimeMillis(); + int lastDotIndex = fullClassName.lastIndexOf("."); + String className = fullClassName.substring(lastDotIndex + 1); + return new TraceStatus(id, startTime, className, method); + } + + @SneakyThrows + public void end(TraceStatus traceStatus) { + final LogFormat logFormat = LogFormat.createLogFormat(traceStatus); + final String logMessage = objectMapper.writeValueAsString(logFormat); + if (logFormat.getExecuteTime() >= WARN_REQUEST_TIME) { + log.warn(logMessage); + } else { + log.info(logMessage); + } + } + + @SneakyThrows + public void exception(Exception exception, TraceStatus traceStatus){ + final LogFormat errorLogFormat = LogFormat.createErrorLogFormat(traceStatus, exception); + final String errorLog = objectMapper.writeValueAsString(errorLogFormat); + log.error(errorLog); + } + + public void configThreadId() { + threadId.set(createThreadId()); + } + + public void clearTheadId() { + threadId.remove(); + } + + public String getThreadId() { + return threadId.get(); + } + + private String createThreadId() { + return UUID.randomUUID().toString().substring(0, 8); + } + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/log/aop/Pointcuts.java b/src/main/java/com/moing/backend/global/log/aop/Pointcuts.java new file mode 100644 index 00000000..3dd7887c --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/aop/Pointcuts.java @@ -0,0 +1,17 @@ +package com.moing.backend.global.log.aop; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; + +@Aspect +public class Pointcuts { + + + @Pointcut("execution(* com.moing.backend.*presentation.*Controller.*(..))") + public void allController() {} + + @Pointcut("execution(* com.moing.backend..service..*.*(..))") + public void allService() { + } + +} diff --git a/src/main/java/com/moing/backend/global/log/aop/TraceStatus.java b/src/main/java/com/moing/backend/global/log/aop/TraceStatus.java new file mode 100644 index 00000000..b7d98520 --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/aop/TraceStatus.java @@ -0,0 +1,15 @@ +package com.moing.backend.global.log.aop; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +@AllArgsConstructor +public class TraceStatus { + private String threadId; + private Long startTime; + private String className; + private String methodName; +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/log/filter/LogThreadIdHandleFilter.java b/src/main/java/com/moing/backend/global/log/filter/LogThreadIdHandleFilter.java new file mode 100644 index 00000000..ad8873d4 --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/filter/LogThreadIdHandleFilter.java @@ -0,0 +1,51 @@ +package com.moing.backend.global.log.filter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moing.backend.global.log.aop.LogTrace; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + + +@Slf4j +@Order(Integer.MIN_VALUE) +@Component +@RequiredArgsConstructor +public class LogThreadIdHandleFilter implements Filter { + + private final ObjectMapper objectMapper; + private final LogTrace logTrace; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Filter.super.init(filterConfig); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + logTrace.configThreadId(); + log.info(buildRequestInfoMessage((HttpServletRequest) request)); + chain.doFilter(request, response); + logTrace.clearTheadId(); + } + + private String buildRequestInfoMessage(HttpServletRequest request) throws JsonProcessingException { + final RequestInfoFormat requestInfoFormat = RequestInfoFormat.builder() + .threadId(logTrace.getThreadId()) + .url(request.getRequestURI()) + .method(request.getMethod()) + .build(); + return objectMapper.writeValueAsString(requestInfoFormat); + } + + @Override + public void destroy() { + Filter.super.destroy(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/log/filter/RequestInfoFormat.java b/src/main/java/com/moing/backend/global/log/filter/RequestInfoFormat.java new file mode 100644 index 00000000..030eb2fc --- /dev/null +++ b/src/main/java/com/moing/backend/global/log/filter/RequestInfoFormat.java @@ -0,0 +1,18 @@ +package com.moing.backend.global.log.filter; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RequestInfoFormat { + private final String threadId; + private final String url; + private final String method; + private final String ip; +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/response/BaseBoardServiceResponse.java b/src/main/java/com/moing/backend/global/response/BaseBoardServiceResponse.java new file mode 100644 index 00000000..c65abe5b --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/BaseBoardServiceResponse.java @@ -0,0 +1,19 @@ +package com.moing.backend.global.response; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class BaseBoardServiceResponse { + private Member member; + private Team team; + private Board board; + private TeamMember teamMember; +} diff --git a/src/main/java/com/moing/backend/global/response/BaseMissionServiceResponse.java b/src/main/java/com/moing/backend/global/response/BaseMissionServiceResponse.java new file mode 100644 index 00000000..f0ee645d --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/BaseMissionServiceResponse.java @@ -0,0 +1,19 @@ +package com.moing.backend.global.response; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class BaseMissionServiceResponse { + private Member member; + private Team team; + private MissionArchive missionArchive; + private TeamMember teamMember; +} diff --git a/src/main/java/com/moing/backend/global/response/BaseServiceResponse.java b/src/main/java/com/moing/backend/global/response/BaseServiceResponse.java new file mode 100644 index 00000000..1258c29b --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/BaseServiceResponse.java @@ -0,0 +1,17 @@ +package com.moing.backend.global.response; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class BaseServiceResponse { + private Member member; + private Team team; + private TeamMember teamMember; +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/response/ErrorCode.java b/src/main/java/com/moing/backend/global/response/ErrorCode.java new file mode 100644 index 00000000..72516a96 --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/ErrorCode.java @@ -0,0 +1,77 @@ +package com.moing.backend.global.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + + BAD_REQUEST("400", "입력값이 유효하지 않습니다."), + METHOD_NOT_ALLOWED("405", "클라이언트가 사용한 HTTP 메서드가 리소스에서 허용되지 않습니다."), + INTERNAL_SERVER_ERROR("500", "서버에서 요청을 처리하는 동안 오류가 발생했습니다."), + NOT_FOUND_REFRESH_TOKEN_ERROR( "J0008", "유효하지 않는 RefreshToken 입니다."), + + //FCM 토큰 관련 + INITIALIZE_ERROR("F0001", "Firebase Admin SDK 초기화에 실패했습니다."), + NOTIFICATION_ERROR("F0002", "메시지 전송에 실패했습니다."), + MESSAGING_ERROR("F0003", "firebaseConfigPath를 읽어오는데 실패하였습니다"), + + //유저 관련 에러 코드 + NOT_FOUND_BY_SOCIAL_ID_ERROR( "U0001", "해당 socialId인 유저가 존재하지 않습니다."), + ACCOUNT_ALREADY_EXIST("AU0001", "해당 email로 다른 소셜 플랫폼으로 가입하였습니다."), + TOKEN_INVALID_ERROR("AU0002", "입력 토큰이 유효하지 않습니다."), + APPID_INVALID_ERROR("AU0003", "appId가 유효하지 않습니다"), + NICKNAME_DUPLICATION_ERROR("AU0004", "닉네임이 중복됩니다."), + NOT_FOUND_ALL_MEMBER("AU0005","푸시 알림을 위한 모든 멤버를 불러오는데 실패했습니다."), + + //미션 관련 에러코드 + NO_ACCESS_CREATE_MISSION("M0001", "소모임장만 미션을 생성할 수 있습니다."), + NO_ACCESS_UPDATE_MISSION("M0001", "미션 생성자 또는 소모임장만 미션을 수정할 수 있습니다."), + NO_ACCESS_DELETE_MISSION("M0001", "미션 생성자 또는 소모임장만 미션을 삭제할 수 있습니다."), + NOT_FOUND_MISSION("M0002", "미션을 찾을 수 없습니다."), + NOT_FOUND_END_MISSION("M0003", "기한이 지난 미션을 찾을 수 없습니다."), + NO_MORE_CREATE_MISSION("M0004", "반복미션은 2개까지 생성할 수 있습니다."), + NOT_FOUND_MISSION_ARCHIVE("MA0001", "아직 미션을 제출하지 않았습니다."), + NOT_YET_MISSION_ARCHIVE("MA0001", "아직 미션을 제출할 수 없습니다."), + NO_MORE_ARCHIVE_ERROR("MA0001", "지정한 횟수 이상 미션을 인증할 수 없습니다."), + + //불던지기 관련 에러 코드 + NOT_FOUND_FIRE("F001","불던지기 현황을 찾을 수 없습니다"), + NOT_FOUND_FIRE_RECEIVERS("F001","불던지기를 받을 사람을 찾을 수 없습니다"), + NOT_AUTH_FIRE_THROW("F002","1시간 이내에 불던지기를 할 수 없습니다"), + + NO_ACCESS_HEART_FOR_ME("MH001", "나에게 좋아요를 누를 수 없습니다"), + + //팀 관련 에러 코드 + NOT_FOUND_BY_TEAM_ID_ERROR("T0001", "해당 teamId인 팀이 존재하지 않습니다."), + NOT_AUTH_BY_TEAM_ERROR("T0002","권한이 없습니다."), + ALREADY_WITHDRAW_ERROR("T0003","이미 탈퇴한 회원입니다."), + ALREADY_JOIN_ERROR("T0004","이미 가입한 회원입니다."), + DELETED_TEAM_ERROR("T0005", "삭제된 소모임입니다."), + + //마이페이지 관련 에러 코드 + INVALID_ALARM_ERROR("MP0001","유효하지 않는 알람 입력값입니다"), + EXISTING_TEAM_ERROR("MP0002","탈퇴되지 않은 소모임이 있습니다."), + + //게시글 관련 에러 코드 + NOT_FOUND_BY_BOARD_ID_ERROR("B0001","해당 boardId인 게시글이 존재하지 않습니다."), + NOT_AUTH_BY_BOARD_ID_ERROR("B0002","권한이 없습니다."), + + //게시글 댓글 관련 에러 코드 + NOT_FOUND_BY_BOARD_COMMENT_ID_ERROR("BC0001","해당 boardCommentId인 댓글이 존재하지 않습니다."), + NOT_AUTH_BY_BOARD_COMMENT_ID_ERROR("BC0002","권한이 없습니다."), + + //미션 댓글 관련 에러 코드 + NOT_FOUND_BY_MISSION_COMMENT_ID_ERROR("MC0001", "해당 missionCommentId인 댓글이 존재하지 않습니다."), + NOT_AUTH_BY_MISSION_COMMENT_ID_ERROR("MC0002", "권한이 없습니다."), + + //알림 관련 에러 코드 + NOT_FOUND_BY_ALARM_HISOTRY_ID_ERROR("AH0001","해당 alarmHistoryId인 알림이 존재하지 않습니다."), + + //차단 관련 에러 코드 + NOT_FOUND_BLOCK_LIST("BL0001","차단한 사용자를 찾을 수 없습니다"); + private String errorCode; + private String message; + +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/response/ErrorResponse.java b/src/main/java/com/moing/backend/global/response/ErrorResponse.java new file mode 100644 index 00000000..c4d80e55 --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/ErrorResponse.java @@ -0,0 +1,35 @@ +package com.moing.backend.global.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class ErrorResponse { + private Boolean isSuccess; + private LocalDateTime timeStamp; + private String errorCode; + private String message; + + public ErrorResponse(String errorCode, String message) { + this.isSuccess=false; + this.timeStamp = LocalDateTime.now().withNano(0); + this.errorCode = errorCode; + this.message = message; + } + + public ErrorResponse(ErrorCode errorCode, String message) { + this.isSuccess=false; + this.timeStamp = LocalDateTime.now().withNano(0); + this.errorCode=errorCode.getErrorCode(); + this.message=message; + } + public ErrorResponse(ErrorCode errorCode) { + this.isSuccess=false; + this.timeStamp = LocalDateTime.now().withNano(0); + this.errorCode=errorCode.getErrorCode(); + this.message=errorCode.getMessage(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/response/SuccessResponse.java b/src/main/java/com/moing/backend/global/response/SuccessResponse.java new file mode 100644 index 00000000..20353aa0 --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/SuccessResponse.java @@ -0,0 +1,24 @@ +package com.moing.backend.global.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class SuccessResponse { + private Boolean isSuccess; + private String message; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T data; + + public static SuccessResponse create(String message) { + return new SuccessResponse<>(true, message, null); + } + + public static SuccessResponse create(String message, T data) { + return new SuccessResponse<>(true, message, data); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/response/TokenInfoResponse.java b/src/main/java/com/moing/backend/global/response/TokenInfoResponse.java new file mode 100644 index 00000000..d0edd5a8 --- /dev/null +++ b/src/main/java/com/moing/backend/global/response/TokenInfoResponse.java @@ -0,0 +1,29 @@ +package com.moing.backend.global.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@AllArgsConstructor +public class TokenInfoResponse { + private String grantType; + private String accessToken; + private String refreshToken; + private Long refreshTokenExpirationTime; + + public static TokenInfoResponse from(String grantType, String accessToken, String refreshToken, Long refreshTokenExpirationTime) { + return TokenInfoResponse.builder() + .grantType(grantType) + .accessToken(accessToken) + .refreshToken(refreshToken) + .refreshTokenExpirationTime(refreshTokenExpirationTime) + .build(); + } + + public void updateRefreshToken(String refreshToken){ + this.refreshToken=refreshToken; + } +} + diff --git a/src/main/java/com/moing/backend/global/utils/AesConverter.java b/src/main/java/com/moing/backend/global/utils/AesConverter.java new file mode 100644 index 00000000..f3208b2a --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/AesConverter.java @@ -0,0 +1,37 @@ +package com.moing.backend.global.utils; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class AesConverter implements AttributeConverter { + private final AesUtil aesUtil; + + public AesConverter(AesUtil aesUtil) { + this.aesUtil = aesUtil; + } + + @Override + public String convertToDatabaseColumn(String attribute) { + if (attribute == null) { + return null; // attribute가 null인 경우 null 반환 + } + try { + return aesUtil.encrypt(attribute); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to encrypt attribute", e); + } + } + + @Override + public String convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; // dbData가 null인 경우 null 반환 + } + try { + return aesUtil.decrypt(dbData); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to decrypt dbData", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/utils/AesUtil.java b/src/main/java/com/moing/backend/global/utils/AesUtil.java new file mode 100644 index 00000000..c9423f40 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/AesUtil.java @@ -0,0 +1,51 @@ +package com.moing.backend.global.utils; + +import com.moing.backend.global.annotation.Util; +import org.springframework.beans.factory.annotation.Value; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@Util +public class AesUtil { + private final String key; + private final String alg; + private final String iv; + + public AesUtil(@Value("${aes.secret.key}") String key, @Value("${aes.secret.alg}") String alg) { + this.key = key; + this.alg = alg; + this.iv = key.substring(0, 16); + } + + public String encrypt(String text) { + try { + Cipher cipher = Cipher.getInstance(alg); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); + IvParameterSpec ivParamSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8)); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParamSpec); + byte[] encrypted = cipher.doFinal(text.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encrypted); + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt text: " + e.getMessage(), e); + } + } + + public String decrypt(String cipherText) { + try { + Cipher cipher = Cipher.getInstance(alg); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); + IvParameterSpec ivParamSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8)); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParamSpec); + + byte[] decodedBytes = Base64.getDecoder().decode(cipherText); + byte[] decrypted = cipher.doFinal(decodedBytes); + return new String(decrypted, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("Failed to decrypt cipherText: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/moing/backend/global/utils/BaseBoardService.java b/src/main/java/com/moing/backend/global/utils/BaseBoardService.java new file mode 100644 index 00000000..f99ffac8 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/BaseBoardService.java @@ -0,0 +1,32 @@ +package com.moing.backend.global.utils; + +import com.moing.backend.domain.board.domain.entity.Board; +import com.moing.backend.domain.board.domain.service.BoardGetService; +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.response.BaseBoardServiceResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class BaseBoardService { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final BoardGetService boardGetService; + private final TeamMemberGetService teamMemberGetService; + + public BaseBoardServiceResponse getCommonData(String socialId, Long teamId, Long boardId) { + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = teamGetService.getTeamByTeamId(teamId); + Board board = boardGetService.getBoard(boardId); + TeamMember teamMember = teamMemberGetService.getTeamMember(member, team); + + return new BaseBoardServiceResponse(member, team, board, teamMember); + } +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/utils/BaseMissionService.java b/src/main/java/com/moing/backend/global/utils/BaseMissionService.java new file mode 100644 index 00000000..84311b40 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/BaseMissionService.java @@ -0,0 +1,32 @@ +package com.moing.backend.global.utils; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveQueryService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.response.BaseMissionServiceResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class BaseMissionService { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final MissionArchiveQueryService missionArchiveQueryService; + private final TeamMemberGetService teamMemberGetService; + + public BaseMissionServiceResponse getCommonData(String socialId, Long teamId, Long missionArchiveId){ + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = teamGetService.getTeamByTeamId(teamId); + MissionArchive missionArchive=missionArchiveQueryService.findByMissionArchiveId(missionArchiveId); + TeamMember teamMember = teamMemberGetService.getTeamMember(member, team); + + return new BaseMissionServiceResponse(member, team, missionArchive, teamMember); + } +} diff --git a/src/main/java/com/moing/backend/global/utils/BaseService.java b/src/main/java/com/moing/backend/global/utils/BaseService.java new file mode 100644 index 00000000..f6fd8513 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/BaseService.java @@ -0,0 +1,28 @@ +package com.moing.backend.global.utils; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.domain.member.domain.service.MemberGetService; +import com.moing.backend.domain.team.domain.entity.Team; +import com.moing.backend.domain.team.domain.service.TeamGetService; +import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +import com.moing.backend.domain.teamMember.domain.service.TeamMemberGetService; +import com.moing.backend.global.response.BaseServiceResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class BaseService { + + private final MemberGetService memberGetService; + private final TeamGetService teamGetService; + private final TeamMemberGetService teamMemberGetService; + + public BaseServiceResponse getCommonData(String socialId, Long teamId) { + Member member = memberGetService.getMemberBySocialId(socialId); + Team team = teamGetService.getTeamByTeamId(teamId); + TeamMember teamMember = teamMemberGetService.getTeamMember(member, team); + + return new BaseServiceResponse(member, team, teamMember); + } +} diff --git a/src/main/java/com/moing/backend/global/utils/FeignClientConfig.java b/src/main/java/com/moing/backend/global/utils/FeignClientConfig.java new file mode 100644 index 00000000..2c7f1f20 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/FeignClientConfig.java @@ -0,0 +1,7 @@ +package com.moing.backend.global.utils; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FeignClientConfig { +} \ No newline at end of file diff --git a/src/main/java/com/moing/backend/global/utils/SecurityUtils.java b/src/main/java/com/moing/backend/global/utils/SecurityUtils.java new file mode 100644 index 00000000..0c901985 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/SecurityUtils.java @@ -0,0 +1,25 @@ +package com.moing.backend.global.utils; + +import com.moing.backend.domain.member.domain.entity.Member; +import com.moing.backend.global.config.security.oauth.CustomUserDetails; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Transactional +public class SecurityUtils { + + public static Member getLoggedInUser() { + try { + return + ((CustomUserDetails) Objects.requireNonNull(SecurityContextHolder.getContext().getAuthentication()).getPrincipal()).getMember(); + } catch (NullPointerException e) { + throw new RuntimeException(); + } + } + +} diff --git a/src/main/java/com/moing/backend/global/utils/StartupRunner.java b/src/main/java/com/moing/backend/global/utils/StartupRunner.java new file mode 100644 index 00000000..7857d309 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/StartupRunner.java @@ -0,0 +1,43 @@ +//package com.moing.backend.global.utils; +// +//import com.moing.backend.domain.member.domain.constant.Gender; +//import com.moing.backend.domain.member.domain.constant.RegistrationStatus; +//import com.moing.backend.domain.member.domain.constant.Role; +//import com.moing.backend.domain.member.domain.constant.SocialProvider; +//import com.moing.backend.domain.member.domain.entity.Member; +//import com.moing.backend.domain.member.domain.service.MemberSaveService; +//import com.moing.backend.global.annotation.Util; +//import lombok.RequiredArgsConstructor; +//import org.springframework.boot.CommandLineRunner; +//import org.springframework.stereotype.Component; +// +//import java.time.LocalDate; +// +//@Util +//@RequiredArgsConstructor +//public class StartupRunner implements CommandLineRunner { +// +// /** +// * 테스트용 insert +// */ +// +// private final MemberSaveService memberSaveService; +// @Override +// public void run(String... args) throws Exception { +// Member test01 = new Member(LocalDate.now(), "tester1@test.com", "undef", Gender.WOMAN, "undef", "modagbul_tester1", "undef", SocialProvider.KAKAO, RegistrationStatus.COMPLETED, Role.USER, "KAKAO@tester01"); +// memberSaveService.saveMember(test01); +// +// Member test02=new Member(LocalDate.now(), "tester2@test.com", "undef", Gender.MAN, null, "modagbul_tester2", null, SocialProvider.KAKAO, RegistrationStatus.COMPLETED, Role.USER, "KAKAO@tester02"); +// memberSaveService.saveMember(test02); +// +// Member test03=new Member(LocalDate.now(), "tester3@test.com", "undef", Gender.WOMAN, "undef", "modagbul_tester3", "undef", SocialProvider.APPLE, RegistrationStatus.COMPLETED, Role.USER, "APPLE@tester03"); +// memberSaveService.saveMember(test03); +// +// Member test04=new Member(LocalDate.now(), "tester4@test.com", "undef", Gender.WOMAN, "undef", "modagbul_tester4", "undef", SocialProvider.APPLE, RegistrationStatus.COMPLETED, Role.USER, "APPLE@tester04"); +// memberSaveService.saveMember(test04); +// +// Member test05=new Member(LocalDate.now(), "tester5@test.com", "undef", Gender.WOMAN, "undef", "modagbul_tester5", "undef", SocialProvider.APPLE, RegistrationStatus.COMPLETED, Role.USER, "APPLE@tester05"); +// memberSaveService.saveMember(test05); +// +// } +//} diff --git a/src/main/java/com/moing/backend/global/utils/UpdateUtils.java b/src/main/java/com/moing/backend/global/utils/UpdateUtils.java new file mode 100644 index 00000000..246ce8e2 --- /dev/null +++ b/src/main/java/com/moing/backend/global/utils/UpdateUtils.java @@ -0,0 +1,32 @@ +package com.moing.backend.global.utils; + +import com.moing.backend.global.annotation.Util; +import com.moing.backend.global.config.s3.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Util +@RequiredArgsConstructor +public class UpdateUtils { + + private final S3Service s3Service; + + public static T getUpdatedValue(T currentValue, T oldValue) { + if(currentValue!=null){ + return currentValue; + } + return oldValue; + } + + public void deleteOldImgUrl(String currentImageUrl, String oldImageUrl) { + if (currentImageUrl != null && oldImageUrl != null) { + s3Service.deleteImage(oldImageUrl); + } + } + + public void deleteImgUrl(String imageUrl) { + if (imageUrl != null) { + s3Service.deleteImage(imageUrl); + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000..fb6ab4bc --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + + + .\logs\logback\logback-%d{yyyy-MM-dd}_%i.log + + 50MB + + + + ${SAVE_LOG_PATTERN} + + + + + ${LOG_PATTERN} + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/moing/backend/BackendApplicationTests.java b/src/test/java/com/moing/backend/BackendApplicationTests.java new file mode 100644 index 00000000..0170d440 --- /dev/null +++ b/src/test/java/com/moing/backend/BackendApplicationTests.java @@ -0,0 +1,13 @@ +package com.moing.backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +//@SpringBootTest +class BackendApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/moing/backend/config/CommonControllerTest.java b/src/test/java/com/moing/backend/config/CommonControllerTest.java new file mode 100644 index 00000000..9710cdb2 --- /dev/null +++ b/src/test/java/com/moing/backend/config/CommonControllerTest.java @@ -0,0 +1,85 @@ +package com.moing.backend.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moing.backend.domain.member.domain.constant.Role; +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.filter.JwtAccessDeniedHandler; +import com.moing.backend.global.config.security.filter.JwtAuthenticationEntryPoint; +import com.moing.backend.global.config.security.jwt.TokenUtil; +import com.moing.backend.global.config.security.util.AuthenticationUtil; +import com.moing.backend.global.log.aop.LogTrace; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +@AutoConfigureRestDocs +@Import(RestDocsConfig.class) +@MockBean(JpaMetamodelMappingContext.class) +@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) +public class CommonControllerTest { + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + public TokenUtil tokenUtil; + + @MockBean + public JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @MockBean + public ClientRegistrationRepository clientRegistrationRepository; + + @MockBean + public JwtAccessDeniedHandler jwtAccessDeniedHandler; + + @MockBean + public MemberGetService memberQueryService; + + @MockBean + public LogTrace logTrace; + + @BeforeEach + public void setUp(final WebApplicationContext context, final RestDocumentationContextProvider provider) throws Exception { + + Member member = Member.builder() + .memberId(1L) + .email("test@test.com") + .role(Role.USER) + .socialId("KAKAO@123") + .build(); + + // user.getSocialId() 에서 NullPointerException 방지를 위한 Authentication 생성 + AuthenticationUtil.makeAuthentication(member); + + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) // rest docs 설정 주입 + .alwaysDo(MockMvcResultHandlers.print()) // andDo(print()) 코드 포함 + .alwaysDo(restDocs) // pretty 패턴과 문서 디렉토리 명 정해준것 적용 + .addFilters(new CharacterEncodingFilter("UTF-8", true)) // 한글 깨짐 방지 + .build(); + } +} + diff --git a/src/test/java/com/moing/backend/config/RestDocsConfig.java b/src/test/java/com/moing/backend/config/RestDocsConfig.java new file mode 100644 index 00000000..3fb4a9f1 --- /dev/null +++ b/src/test/java/com/moing/backend/config/RestDocsConfig.java @@ -0,0 +1,19 @@ +package com.moing.backend.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.operation.preprocess.Preprocessors; + +@TestConfiguration +public class RestDocsConfig { + @Bean + public RestDocumentationResultHandler write(){ + return MockMvcRestDocumentation.document( + "{class-name}/{method-name}", + Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), + Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) + ); + } +} diff --git a/src/test/java/com/moing/backend/domain/auth/presentation/AuthControllerTest.java b/src/test/java/com/moing/backend/domain/auth/presentation/AuthControllerTest.java new file mode 100644 index 00000000..434e4321 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/auth/presentation/AuthControllerTest.java @@ -0,0 +1,458 @@ +package com.moing.backend.domain.auth.presentation; + +import com.moing.backend.config.CommonControllerTest; +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.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.domain.member.domain.constant.Gender; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(AuthController.class) +class AuthControllerTest extends CommonControllerTest { + + @MockBean + private SignInUseCase authService; + + @MockBean + private SignUpUseCase signUpUseCase; + + @MockBean + private ReissueTokenUseCase reissueTokenUseCase; + + @MockBean + private CheckNicknameUseCase checkNicknameService; + + @Test + public void Kakao_소셜_로그인_회원가입_전() throws Exception { + //given + SignInRequest input = SignInRequest.builder() + .fcmToken("FCM_TOKEN") + .socialToken("KAKAO_ACCESS_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(false) + .build(); + + given(authService.signIn(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/auth/signIn/kakao") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("fcmToken").description("FCM TOKEN"), + fieldWithPath("socialToken").description("카카오 액세스 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 : false") + ) + ) + ); + } + + @Test + public void Kakao_소셜_로그인_회원가입_후() throws Exception { + //given + SignInRequest input = SignInRequest.builder() + .fcmToken("FCM_TOKEN") + .socialToken("KAKAO_ACCESS_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(true) + .build(); + + given(authService.signIn(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/auth/signIn/kakao") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("fcmToken").description("FCM TOKEN"), + fieldWithPath("socialToken").description("카카오 액세스 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 :true") + ) + ) + ); + } + + @Test + public void Apple_소셜_로그인_회원가입_전() throws Exception { + //given + SignInRequest input = SignInRequest.builder() + .fcmToken("FCM_TOKEN") + .socialToken("APPLE_IDENTITY_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(false) + .build(); + + given(authService.signIn(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/auth/signIn/apple") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("fcmToken").description("FCM TOKEN"), + fieldWithPath("socialToken").description("애플 아이디 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 :false") + ) + ) + ); + } + + @Test + public void Apple_소셜_로그인_회원가입_후() throws Exception { + //given + SignInRequest input = SignInRequest.builder() + .fcmToken("FCM_TOKEN") + .socialToken("APPLE_IDENTITY_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(true) + .build(); + + given(authService.signIn(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/auth/signIn/apple") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("fcmToken").description("FCM TOKEN"), + fieldWithPath("socialToken").description("애플 아이디 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 :true") + ) + ) + ); + } + + @Test + public void GOOGLE_소셜_로그인_회원가입_전() throws Exception { + //given + SignInRequest input = SignInRequest.builder() + .fcmToken("FCM_TOKEN") + .socialToken("APPLE_IDENTITY_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(false) + .build(); + + given(authService.signIn(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/auth/signIn/google") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("fcmToken").description("FCM TOKEN"), + fieldWithPath("socialToken").description("애플 아이디 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 :false") + ) + ) + ); + } + + @Test + public void GOOGLE_소셜_로그인_회원가입_후() throws Exception { + //given + SignInRequest input = SignInRequest.builder() + .fcmToken("FCM_TOKEN") + .socialToken("APPLE_IDENTITY_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(true) + .build(); + + given(authService.signIn(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/auth/signIn/google") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("fcmToken").description("FCM TOKEN"), + fieldWithPath("socialToken").description("애플 아이디 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 :true") + ) + ) + ); + } + + + @Test + public void sign_up() throws Exception { + //given + SignUpRequest input = SignUpRequest.builder() + .nickName("NICKNAME") + .gender(Gender.MAN) + .birthDate("2000-03-28") + .build(); + + String body = objectMapper.writeValueAsString(input); + + SignInResponse output = SignInResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .registrationStatus(true) + .build(); + + + given(signUpUseCase.signUp(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + put("/api/auth/signUp") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + requestFields( + fieldWithPath("nickName").description("유저 닉네임"), + fieldWithPath("gender").description("성별"), + fieldWithPath("birthDate").description("태어난 날짜(YYYY-MM-DD)") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("회원 가입을 했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Access Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token"), + fieldWithPath("data.registrationStatus").description("회원가입 여부 : true") + ) + ) + ); + } + @Test + public void reissue_token() throws Exception { + //given + ReissueTokenResponse output = ReissueTokenResponse.builder() + .accessToken("SERVER_ACCESS_TOKEN") + .refreshToken("SERVER_REFRESH_TOKEN") + .build(); + + given(reissueTokenUseCase.reissueToken(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + get("/api/auth/reissue") + .header("RefreshToken", "REFRESH_TOKEN") + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("RefreshToken").description("토큰 재발급용 RefreshToken") + ), + responseFields( + fieldWithPath("isSuccess").description("성공 여부 : true"), + fieldWithPath("message").description("토큰을 재발급했습니다"), + fieldWithPath("data.accessToken").description("서버 접근용 Token"), + fieldWithPath("data.refreshToken").description("서버 접근용 Refresh Token") + ) + ) + ); + } + + @Test + public void check_nickname_중복O() throws Exception { + //given + CheckNicknameResponse output = new CheckNicknameResponse(true); + given(checkNicknameService.checkNickname(any())).willReturn(output); + + + // when + ResultActions actions = mockMvc.perform( + get("/api/auth/checkNickname?nickname=minsu") + ); + + // then + actions + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestParameters( + parameterWithName("nickname").description("중복검사할 닉네임") + ), + responseFields( + fieldWithPath("isSuccess").description("성공 여부 : true"), + fieldWithPath("message").description("닉네임 중복검사를 했습니다"), + fieldWithPath("data.isDuplicated").description("닉네임 중복 여부 : ture") // 이 부분은 CheckNicknameResponse의 구조에 따라 변경될 수 있습니다. + ) + )); + } + + @Test + public void check_nickname_중복X() throws Exception { + //given + CheckNicknameResponse output = new CheckNicknameResponse(false); + given(checkNicknameService.checkNickname(any())).willReturn(output); + + + // when + ResultActions actions = mockMvc.perform( + get("/api/auth/checkNickname?nickname=minsu") + ); + + // then + actions + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestParameters( + parameterWithName("nickname").description("중복검사할 닉네임") + ), + responseFields( + fieldWithPath("isSuccess").description("성공 여부 : true"), + fieldWithPath("message").description("닉네임 중복검사를 했습니다"), + fieldWithPath("data.isDuplicated").description("닉네임 중복 여부 : false") // 이 부분은 CheckNicknameResponse의 구조에 따라 변경될 수 있습니다. + ) + )); + } + +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/block/presentation/BlockControllerTest.java b/src/test/java/com/moing/backend/domain/block/presentation/BlockControllerTest.java new file mode 100644 index 00000000..2d9b9d47 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/block/presentation/BlockControllerTest.java @@ -0,0 +1,205 @@ +package com.moing.backend.domain.block.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.block.application.service.BlockCreateUseCase; +import com.moing.backend.domain.block.application.service.BlockDeleteUseCase; +import com.moing.backend.domain.block.application.service.BlockReadUseCase; +import com.moing.backend.domain.report.application.dto.BlockMemberRes; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.assertj.core.util.Lists; + +import java.util.ArrayList; +import java.util.List; + +import static com.moing.backend.domain.block.presentation.constant.BlockResponseMessage.*; +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.CREATE_REPORT_SUCCESS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +@WebMvcTest(BlockController.class) +class BlockControllerTest extends CommonControllerTest { + + @MockBean + private BlockReadUseCase blockReadUseCase; + @MockBean + private BlockCreateUseCase blockCreateUseCase; + @MockBean + private BlockDeleteUseCase blockDeleteUseCase; + + @Test + public void 차단_하기() throws Exception{ + + Long targetId = 1L; + + given(blockCreateUseCase.createBlock(any(),any())).willReturn(targetId); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/block/{targetId}", targetId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + pathParameters( + parameterWithName("targetId").description("신고할 사용자 아이디") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(CREATE_BLOCK_SUCCESS.getMessage()), + fieldWithPath("data").description("신고한 유저 번호") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 차단_해제_하기() throws Exception{ + + Long targetId = 1L; + + given(blockDeleteUseCase.deleteBlock(any(),any())).willReturn(targetId); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/block/{targetId}", targetId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + pathParameters( + parameterWithName("targetId").description("신고할 사용자 아이디") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(DELETE_BLOCK_SUCCESS.getMessage()), + fieldWithPath("data").description("신고한 유저 번호") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 차단한_유저_목록() throws Exception{ + + + List response = new ArrayList<>(); + + given(blockReadUseCase.getMyBlockList(any())).willReturn(response); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/block/") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(GET_BLOCK_SUCCESS.getMessage()), + fieldWithPath("data").description("차단한 유저 목록") + + ) + ) + ) + .andReturn(); + + } + @Test + public void 차단한_유저_정보_목록() throws Exception{ + + + List output = Lists.newArrayList(BlockMemberRes.builder() + .targetId(1L) + .introduce("차단한 사용자 소개") + .nickName("차단한 사용자 닉네임") + .profileImg("프로필 이미지") + .build()); + + given(blockReadUseCase.getMyBlockInfoList(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/block/info") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(GET_BLOCK_SUCCESS.getMessage()), + fieldWithPath("data[].targetId").description("차단한 유저 아이디"), + fieldWithPath("data[].nickName").description("차단한 유저 닉네임"), + fieldWithPath("data[].profileImg").description("차단한 유저 프로필 이미지"), + fieldWithPath("data[].introduce").description("차단한 유저 소개") + + ) + ) + ) + .andReturn(); + + } + + + +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/board/domain/BoardRepositoryTest.java b/src/test/java/com/moing/backend/domain/board/domain/BoardRepositoryTest.java new file mode 100644 index 00000000..9ccb1d77 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/board/domain/BoardRepositoryTest.java @@ -0,0 +1,178 @@ +//package com.moing.backend.domain.board.domain; +// +//import com.moing.backend.domain.block.domain.entity.Block; +//import com.moing.backend.domain.block.domain.repository.BlockRepository; +//import com.moing.backend.domain.board.application.dto.request.CreateBoardRequest; +//import com.moing.backend.domain.board.application.dto.response.GetAllBoardResponse; +//import com.moing.backend.domain.board.application.mapper.BoardMapper; +//import com.moing.backend.domain.board.domain.entity.Board; +//import com.moing.backend.domain.board.domain.repository.BoardRepository; +//import com.moing.backend.domain.boardRead.domain.entity.BoardRead; +//import com.moing.backend.domain.boardRead.domain.repository.BoardReadRepository; +//import com.moing.backend.domain.member.domain.constant.Gender; +//import com.moing.backend.domain.member.domain.constant.RegistrationStatus; +//import com.moing.backend.domain.member.domain.constant.Role; +//import com.moing.backend.domain.member.domain.constant.SocialProvider; +//import com.moing.backend.domain.member.domain.entity.Member; +//import com.moing.backend.domain.member.domain.repository.MemberRepository; +//import com.moing.backend.domain.team.domain.constant.ApprovalStatus; +//import com.moing.backend.domain.team.domain.entity.Team; +//import com.moing.backend.domain.team.domain.repository.TeamRepository; +//import com.moing.backend.domain.teamMember.domain.entity.TeamMember; +//import com.moing.backend.domain.teamMember.domain.repository.TeamMemberRepository; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +//import org.springframework.transaction.annotation.Transactional; +// +//import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +// +// +//@SpringBootTest +//@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +//@ActiveProfiles("local") +//@Transactional +//public class BoardRepositoryTest { +// +// @Autowired +// MemberRepository memberRepository; +// +// @Autowired +// TeamMemberRepository teamMemberRepository; +// +// @Autowired +// TeamRepository teamRepository; +// +// @Autowired +// BoardRepository boardRepository; +// +// @Autowired +// BoardReadRepository boardReadRepository; +// +// @Autowired +// BlockRepository blockRepository; +// +// private Member checkingMember, member1, member2; +// +// private Team team; +// private TeamMember checkingTM, tm1Deleted, tm2NotDeleted; +// private CreateBoardRequest createBoardRequest; +// +// @BeforeEach +// void setUp() { +// //given +// checkingMember = memberRepository.save(new Member(null, "alstn@naver.com", "undef", Gender.WOMAN, null, "민수", null, SocialProvider.KAKAO, RegistrationStatus.COMPLETED, Role.USER, "KAKAO@alstn")); +// member1 = memberRepository.save(new Member(null, "tmddus@naver.com", "undef", Gender.WOMAN, null, "승연", null, SocialProvider.KAKAO, RegistrationStatus.COMPLETED, Role.USER, "KAKAO@tmddus")); +// member2 = memberRepository.save(new Member(null, "codud@naver.com", "undef", Gender.WOMAN, null, "채영", null, SocialProvider.KAKAO, RegistrationStatus.COMPLETED, Role.USER, "KAKAO@codud")); +// +// team = teamRepository.save(Team.builder() +// .category("ETC") +// .name("소모임 이름") +// .introduction("소모임 소개") +// .promise("소모임 각오") +// .profileImgUrl("소모임 프로필 이미지 url") +// .approvalStatus(ApprovalStatus.APPROVAL) +// .leaderId(1L) +// .numOfMember(2) +// .levelOfFire(1) +// .build()); +// +// checkingTM = teamMemberRepository.save(TeamMember.builder().team(team).member(checkingMember).isDeleted(false).build()); +// tm1Deleted = teamMemberRepository.save(TeamMember.builder().team(team).member(member1).isDeleted(true).build()); +// tm2NotDeleted = teamMemberRepository.save(TeamMember.builder().team(team).member(member2).isDeleted(true).build()); +// +// createBoardRequest = CreateBoardRequest.builder() +// .title("게시글 제목") +// .content("게시글 내용") +// .isNotice(false) +// .build(); +// } +// +// @Test +// @DisplayName("게시글을 읽은 경우") +// void whenBoardIsRead_thenMarkAsRead() { +// //given +// Board board = boardRepository.save(BoardMapper.toBoard(checkingTM, team, createBoardRequest, false)); +// +// boardReadRepository.save(new BoardRead(null, board, team, checkingMember)); +// +// //when +// GetAllBoardResponse response = boardRepository.findBoardAll(team.getTeamId(), checkingMember.getMemberId()); +// +// //then +// assertThat(response.getNotNoticeBlocks().get(0).getIsRead()).isTrue(); +// } +// +// @Test +// @DisplayName("게시글을 읽지 않은 경우") +// void whenBoardIsNotRead_thenMarkAsUnread() { +// //given +// Board board = boardRepository.save(BoardMapper.toBoard(checkingTM, team, createBoardRequest, false)); +// +// //when +// GetAllBoardResponse response = boardRepository.findBoardAll(team.getTeamId(), checkingMember.getMemberId()); +// +// //then +// assertThat(response.getNotNoticeBlocks().get(0).getIsRead()).isFalse(); +// } +// @Test +// @DisplayName("작성자가 삭제된 경우") +// void 게시글_작성자가_탈퇴하지_않은_경우_게시글_전체_조회() { +// //given +// Board board = boardRepository.save(BoardMapper.toBoard(tm1Deleted, team, createBoardRequest, false)); //작성자 탈퇴한 경우 +// +// //when +// GetAllBoardResponse response = boardRepository.findBoardAll(team.getTeamId(), checkingMember.getMemberId()); +// +// //then +// assertThat(response.getNotNoticeBlocks().get(0).getWriterNickName()).isEqualTo("(알 수 없음)"); +// } +// +// @Test +// @DisplayName("유저 차단 경우 작성자 탈퇴 아님") +// void 유저_탈퇴안했을때_차단_조회() { +// //given +// Board board = boardRepository.save(BoardMapper.toBoard(tm2NotDeleted, team, createBoardRequest, false)); //작성자 탈퇴한 경우 +// Block block = blockRepository.save(new Block(checkingMember.getMemberId(), member2.getMemberId())); +// +// //when +// GetAllBoardResponse response = boardRepository.findBoardAll(team.getTeamId(), checkingMember.getMemberId()); +// +// //then +// assertThat(response.getNotNoticeBlocks().size()).isEqualTo(0); +// +// } +// +// @Test +// @DisplayName("유저 차단 경우 작성자 탈퇴") +// void 유저_탈퇴했을때_차단_조회() { +// //given +// Board board = boardRepository.save(BoardMapper.toBoard(tm1Deleted, team, createBoardRequest, false)); //작성자 탈퇴한 경우 +// Block block = blockRepository.save(new Block(checkingMember.getMemberId(), member1.getMemberId())); +// +// //when +// GetAllBoardResponse response = boardRepository.findBoardAll(team.getTeamId(), checkingMember.getMemberId()); +// +// //then +// assertThat(response.getNotNoticeBlocks().size()).isEqualTo(0); +// } +// +// @Test +// @DisplayName("유저 차단 경우 목표보드 게시글 개수") +// void 유저_차단_게시글_조회(){ +// //given +// Board board = boardRepository.save(BoardMapper.toBoard(tm2NotDeleted, team, createBoardRequest, false)); //작성자 탈퇴한 경우 +// Block block = blockRepository.save(new Block(checkingMember.getMemberId(), member2.getMemberId())); +// +// //when +// Integer unReadBoardNum= boardRepository.findUnReadBoardNum(team.getTeamId(), checkingMember.getMemberId()); +// +// //then +// assertThat(unReadBoardNum).isEqualTo(0); +// } +// +//} diff --git a/src/test/java/com/moing/backend/domain/board/presentation/BoardControllerTest.java b/src/test/java/com/moing/backend/domain/board/presentation/BoardControllerTest.java new file mode 100644 index 00000000..6a06f669 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/board/presentation/BoardControllerTest.java @@ -0,0 +1,342 @@ +package com.moing.backend.domain.board.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.board.application.dto.request.CreateBoardRequest; +import com.moing.backend.domain.board.application.dto.request.UpdateBoardRequest; +import com.moing.backend.domain.board.application.dto.response.*; +import com.moing.backend.domain.board.application.service.CreateBoardUseCase; +import com.moing.backend.domain.board.application.service.DeleteBoardUseCase; +import com.moing.backend.domain.board.application.service.GetBoardUseCase; +import com.moing.backend.domain.board.application.service.UpdateBoardUseCase; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(BoardController.class) +public class BoardControllerTest extends CommonControllerTest { + @MockBean + private CreateBoardUseCase createBoardUseCase; + @MockBean + private UpdateBoardUseCase updateBoardUseCase; + @MockBean + private GetBoardUseCase getBoardUseCase; + @MockBean + private DeleteBoardUseCase deleteBoardUseCase; + + @Test + public void create_board() throws Exception { + + //given + Long teamId = 1L; + CreateBoardRequest input = CreateBoardRequest.builder() + .title("게시글 제목") + .content("게시글 내용") + .isNotice(false) + .build(); + + String body = objectMapper.writeValueAsString(input); + + CreateBoardResponse output = CreateBoardResponse.builder() + .boardId(1L) + .build(); + + given(createBoardUseCase.createBoard(any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/{teamId}/board", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + requestFields( + fieldWithPath("title").description("게시글 제목"), + fieldWithPath("content").description("게시글 내용"), + fieldWithPath("isNotice").description("공지 여부") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("게시글을 생성했습니다"), + fieldWithPath("data.boardId").description("생성한 boardId") + ) + ) + ); + } + + @Test + public void update_board() throws Exception { + + //given + Long teamId = 1L; + Long boardId = 1L; + UpdateBoardRequest input = UpdateBoardRequest.builder() + .title("게시글 제목") + .content("게시글 내용") + .isNotice(false) + .build(); + + String body = objectMapper.writeValueAsString(input); + + UpdateBoardResponse output = UpdateBoardResponse.builder() + .boardId(1L) + .build(); + + given(updateBoardUseCase.updateBoard(any(), any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + put("/api/{teamId}/board/{boardId}", teamId, boardId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("boardId").description("게시글 아이디") + ), + requestFields( + fieldWithPath("title").description("게시글 제목"), + fieldWithPath("content").description("게시글 내용"), + fieldWithPath("isNotice").description("공지 여부") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("게시글을 수정했습니다"), + fieldWithPath("data.boardId").description("수정한 boardId") + ) + ) + ); + } + + @Test + public void delete_board() throws Exception { + + //given + Long teamId = 1L; + Long boardId = 1L; + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/{teamId}/board/{boardId}", teamId, boardId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("boardId").description("게시글 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("게시글을 삭제했습니다") + ) + ) + ); + } + + + @Test + public void get_board_all() throws Exception { + //given + List noticeBlocks = new ArrayList<>(); + List notNoticeBlocks = new ArrayList<>(); + Long teamId = 1L; + + BoardBlocks noticeBlock = BoardBlocks.builder() + .boardId(1L) + .writerIsLeader(true) + .writerNickName("작성자 닉네임") + .writerIsDeleted(false) + .writerProfileImage("작성자 프로필 이미지") + .title("공지 제목") + .content("공지 내용") + .commentNum(2) + .isRead(false) + .isNotice(true) + .makerId(1L) + .build(); + + BoardBlocks notNoticeBlock = BoardBlocks.builder() + .boardId(1L) + .writerIsLeader(true) + .writerNickName("작성자 닉네임") + .writerIsDeleted(false) + .writerProfileImage("작성자 프로필 이미지") + .title("게시글 제목") + .content("게시글 내용") + .commentNum(2) + .isRead(false) + .isNotice(false) + .makerId(1L) + .build(); + + noticeBlocks.add(noticeBlock); + notNoticeBlocks.add(notNoticeBlock); + + GetAllBoardResponse output = new GetAllBoardResponse(noticeBlocks.size(), noticeBlocks, notNoticeBlocks.size(), notNoticeBlocks); + + given(getBoardUseCase.getAllBoard(any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/{teamId}/board", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("게시글 목록을 모두 조회했습니다."), + fieldWithPath("data.noticeNum").description("공지 개수"), + fieldWithPath("data.noticeBlocks[].boardId").description("공지 아이디"), + fieldWithPath("data.noticeBlocks[].writerNickName").description("작성자 닉네임"), + fieldWithPath("data.noticeBlocks[].writerIsLeader").description("작성자 소모임장 여부"), + fieldWithPath("data.noticeBlocks[].writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("data.noticeBlocks[].writerIsDeleted").description("작성자 삭제 여부"), + fieldWithPath("data.noticeBlocks[].title").description("공지 제목"), + fieldWithPath("data.noticeBlocks[].content").description("공지 내용"), + fieldWithPath("data.noticeBlocks[].commentNum").description("공지 댓글 개수"), + fieldWithPath("data.noticeBlocks[].isRead").description("공지 읽음 처리 여부"), + fieldWithPath("data.noticeBlocks[].notice").description("true"), + fieldWithPath("data.noticeBlocks[].makerId").description("작성자 Id"), + fieldWithPath("data.notNoticeNum").description("일반 게시글 개수"), + fieldWithPath("data.notNoticeBlocks[].boardId").description("일반 게시글 아이디"), + fieldWithPath("data.notNoticeBlocks[].writerNickName").description("작성자 닉네임"), + fieldWithPath("data.notNoticeBlocks[].writerIsLeader").description("작성자 소모임장 여부"), + fieldWithPath("data.notNoticeBlocks[].writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("data.notNoticeBlocks[].writerIsDeleted").description("작성자 삭제 여부"), + fieldWithPath("data.notNoticeBlocks[].title").description("일반 게시글 제목"), + fieldWithPath("data.notNoticeBlocks[].content").description("일반 게시글 내용"), + fieldWithPath("data.notNoticeBlocks[].commentNum").description("일반 게시글 댓글 개수"), + fieldWithPath("data.notNoticeBlocks[].isRead").description("일반 게시글 읽음 처리 여부"), + fieldWithPath("data.notNoticeBlocks[].notice").description("false"), + fieldWithPath("data.notNoticeBlocks[].makerId").description("작성자 Id") + ) + + ) + ); + } + + @Test + public void get_board_detail() throws Exception { + //given + Long teamId = 1L; + Long boardId=1L; + + GetBoardDetailResponse output = GetBoardDetailResponse.builder() + .boardId(1L) + .writerIsLeader(true) + .writerNickName("작성자 닉네임") + .writerProfileImage("작성자 프로필 이미지") + .title("게시글 제목") + .content("게시글 내용") + .createdDate("2023/09/29 23:42") + .isWriter(false) + .isNotice(false) + .makerId(1L) + .build(); + + given(getBoardUseCase.getBoardDetail(any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/{teamId}/board/{boardId}", teamId, boardId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("boardId").description("게시글 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("게시글 상세 조회했습니다."), + fieldWithPath("data.boardId").description("게시글 아이디"), + fieldWithPath("data.writerNickName").description("작성자 닉네임"), + fieldWithPath("data.writerIsLeader").description("작성자 소모임장 여부"), + fieldWithPath("data.writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("data.title").description("게시글 제목"), + fieldWithPath("data.content").description("게시글 내용"), + fieldWithPath("data.createdDate").description("게시글 생성 날짜, 시간"), + fieldWithPath("data.isWriter").description("게시글 작성자 여부"), + fieldWithPath("data.isNotice").description("게시글 공지 여부"), + fieldWithPath("data.makerId").description("작성자 아이디") + + ) + + ) + ); + } + +} diff --git a/src/test/java/com/moing/backend/domain/boardComment/presentation/BoardCommentControllerTest.java b/src/test/java/com/moing/backend/domain/boardComment/presentation/BoardCommentControllerTest.java new file mode 100644 index 00000000..0d14b897 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/boardComment/presentation/BoardCommentControllerTest.java @@ -0,0 +1,193 @@ +package com.moing.backend.domain.boardComment.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.boardComment.application.service.CreateBoardCommentUseCase; +import com.moing.backend.domain.boardComment.application.service.DeleteBoardCommentUseCase; +import com.moing.backend.domain.boardComment.application.service.GetBoardCommentUseCase; +import com.moing.backend.domain.boardComment.presentattion.BoardCommentController; +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.comment.application.dto.response.CommentBlocks; +import com.moing.backend.domain.comment.application.dto.response.CreateCommentResponse; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(BoardCommentController.class) +public class BoardCommentControllerTest extends CommonControllerTest { + + @MockBean + private CreateBoardCommentUseCase createBoardCommentUseCase; + @MockBean + private DeleteBoardCommentUseCase deleteBoardCommentUseCase; + @MockBean + private GetBoardCommentUseCase getBoardCommentUseCase; + + @Test + public void create_board_comment() throws Exception { + + //given + Long teamId = 1L; + Long boardId = 1L; + CreateCommentRequest input = CreateCommentRequest.builder() + .content("게시글 내용") + .build(); + + String body = objectMapper.writeValueAsString(input); + + CreateCommentResponse output = CreateCommentResponse.builder() + .commentId(1L) + .build(); + + given(createBoardCommentUseCase.createBoardComment(any(), any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/{teamId}/{boardId}/comment", teamId, boardId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("boardId").description("게시글 아이디") + ), + requestFields( + fieldWithPath("content").description("댓글 내용") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("댓글을 생성했습니다"), + fieldWithPath("data.commentId").description("생성한 boardCommentId") + ) + ) + ); + } + + @Test + public void delete_board_comment() throws Exception { + + //given + Long teamId = 1L; + Long boardId = 1L; + Long boardCommentId = 1L; + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/{teamId}/{boardId}/comment/{boardCommentId}", teamId, boardId, boardCommentId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("boardId").description("게시글 아이디"), + parameterWithName("boardCommentId").description("댓글 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("댓글을 삭제했습니다") + ) + ) + ); + } + + + @Test + public void get_board_comment_all() throws Exception { + //given + List commentBlocks = new ArrayList<>(); + Long teamId = 1L; + Long boardId = 1L; + + CommentBlocks commentBlock = CommentBlocks.builder() + .commentId(1L) + .content("댓글 내용") + .writerIsLeader(true) + .writerNickName("작성자 닉네임") + .writerProfileImage("작성자 프로필 이미지") + .writerIsDeleted(false) + .isWriter(true) + .createdDate("2023/12/05 23:29") + .makerId(1L) + .build(); + + commentBlocks.add(commentBlock); + + GetCommentResponse output = new GetCommentResponse(commentBlocks); + + given(getBoardCommentUseCase.getBoardCommentAll(any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/{teamId}/{boardId}/comment", teamId, boardId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("boardId").description("게시글 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("댓글 목록을 모두 조회했습니다."), + fieldWithPath("data.commentBlocks[].commentId").description("댓글 아이디"), + fieldWithPath("data.commentBlocks[].content").description("댓글 내용"), + fieldWithPath("data.commentBlocks[].writerIsLeader").description("작성자 소모임장 여부"), + fieldWithPath("data.commentBlocks[].writerNickName").description("작성자 닉네임"), + fieldWithPath("data.commentBlocks[].writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("data.commentBlocks[].writerIsDeleted").description("작성자 삭제 여부"), + fieldWithPath("data.commentBlocks[].isWriter").description("댓글 작성자 여부"), + fieldWithPath("data.commentBlocks[].createdDate").description("생성 시간"), + fieldWithPath("data.commentBlocks[].makerId").description("작성자 Id") + ) + + ) + ); + } +} diff --git a/src/test/java/com/moing/backend/domain/fire/representation/FireControllerTest.java b/src/test/java/com/moing/backend/domain/fire/representation/FireControllerTest.java new file mode 100644 index 00000000..1ee71150 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/fire/representation/FireControllerTest.java @@ -0,0 +1,144 @@ +package com.moing.backend.domain.fire.representation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.fire.application.dto.req.FireThrowReq; +import com.moing.backend.domain.fire.application.dto.res.FireReceiveRes; +import com.moing.backend.domain.fire.application.dto.res.FireThrowRes; +import com.moing.backend.domain.fire.application.service.FireThrowUseCase; +import com.moing.backend.domain.fire.presentation.FireController; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static com.moing.backend.domain.fire.presentation.constant.FireResponseMessage.GET_RECEIVERS_SUCCESS; +import static com.moing.backend.domain.fire.presentation.constant.FireResponseMessage.THROW_FIRE_SUCCESS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +@WebMvcTest(FireController.class) +public class FireControllerTest extends CommonControllerTest { + + @MockBean + private FireThrowUseCase fireThrowUseCase; + + + @Test + public void 불_던질_사람_조회() throws Exception { + //given + + List output = Lists.newArrayList(FireReceiveRes.builder() + .receiveMemberId(1L) + .nickname("receiver 닉네임") + .fireStatus("True/False") + .profileImg("https://oawijowijfi") + .build()); + + given(fireThrowUseCase.getFireReceiveList(any(),any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}/fire",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(GET_RECEIVERS_SUCCESS.getMessage()), + fieldWithPath("data[].receiveMemberId").description("미션 아이디"), + fieldWithPath("data[].nickname").description("불 받을 사람 "), + fieldWithPath("data[].fireStatus").description("불 던질 수 있는 상태 리턴, 1시간 내 불 던진 내역에 따라 true[True/False]"), + fieldWithPath("data[].profileImg").description("프로필 이미지") + + ) + ) + ) + .andReturn(); + + } + + + @Test + public void 불_던지기() throws Exception { + //given + + FireThrowReq input = FireThrowReq.builder() + .message("불던지기 메시지. 메시지가 없는경우 DTO 자체를 전송하지 않음") + .build(); + + String body = objectMapper.writeValueAsString(input); + + FireThrowRes output = FireThrowRes.builder() + .receiveMemberId(1L) + .build(); + + given(fireThrowUseCase.createFireThrow(any(), any(), any(), any(),any())).willReturn(output); + + Long teamId = 2L; + Long missionId = 2L; + Long receiveMemberId = 2L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/team/{teamId}/missions/{missionId}/fire/{receiveMemberId}",teamId,missionId,receiveMemberId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디"), + parameterWithName("receiveMemberId").description("불 받을 사람 아이디") + ), + requestFields( + fieldWithPath("message").description("불던지기 메시지. 메시지가 없는경우 DTO 자체를 전송하지 않음") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(THROW_FIRE_SUCCESS.getMessage()), + fieldWithPath("data.receiveMemberId").description("미션 아이디") + ) + ) + ) + .andReturn(); + + } + + +} diff --git a/src/test/java/com/moing/backend/domain/history/presentation/AlarmHistoryControllerTest.java b/src/test/java/com/moing/backend/domain/history/presentation/AlarmHistoryControllerTest.java new file mode 100644 index 00000000..4d69766d --- /dev/null +++ b/src/test/java/com/moing/backend/domain/history/presentation/AlarmHistoryControllerTest.java @@ -0,0 +1,155 @@ +package com.moing.backend.domain.history.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.history.application.dto.response.GetAlarmCountResponse; +import com.moing.backend.domain.history.application.dto.response.GetAlarmHistoryResponse; +import com.moing.backend.domain.history.application.service.GetAlarmHistoryUseCase; +import com.moing.backend.domain.history.application.service.ReadAlarmHistoryUseCase; +import com.moing.backend.domain.history.domain.entity.AlarmType; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static com.moing.backend.domain.history.presentation.constant.AlarmHistoryResponseMessage.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +@WebMvcTest(AlarmHistoryController.class) +class AlarmHistoryControllerTest extends CommonControllerTest { + + @MockBean + private GetAlarmHistoryUseCase getAlarmHistoryUseCase; + + @MockBean + private ReadAlarmHistoryUseCase readAlarmHistoryUseCase; + + @Test + public void get_all_alarm_history() throws Exception { + //given + List output = Lists.newArrayList(GetAlarmHistoryResponse.builder() + .alarmHistoryId(1L) + .type(AlarmType.NEW_UPLOAD) + .path("/post/detail") + .idInfo("{\"teamId\":74,\"boardId\":96}") + .title("갓생살자에 새로 올라온 공지를 확인하세요!") + .body("모임 장소 공지") + .name("모닥모닥불") + .isRead(false) + .createdDate("오후 06:39") + .build()); + + given(getAlarmHistoryUseCase.getAllAlarmHistories(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + get("/api/history/alarm") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(GET_ALL_ALARM_HISTORY.getMessage()), + fieldWithPath("data[].alarmHistoryId").description("알림 히스토리 아이디"), + fieldWithPath("data[].type").description("알림 아이콘 구분"), + fieldWithPath("data[].path").description("알림 이동 페이지 path"), + fieldWithPath("data[].idInfo").description("알림 페이지 이동할 때 필요한 data (JSON String)"), + fieldWithPath("data[].title").description("알림의 제목 (가장 bold 처리 된거)"), + fieldWithPath("data[].body").description("알림의 본문 (제목 밑)"), + fieldWithPath("data[].name").description("미션 리마인드 제외하고는 팀 이름 (제목 위)"), + fieldWithPath("data[].createdDate").description("작성 시간"), + fieldWithPath("data[].isRead").description("읽음여부") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void read_alarm_history() throws Exception { + + //when + ResultActions actions = mockMvc.perform( + post("/api/history/alarm/read?alarmHistoryId=1") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(READ_ALARM_HISTORY.getMessage()) + + ) + ) + ) + .andReturn(); + + } + + @Test + public void get_alarm_count() throws Exception { + //given + GetAlarmCountResponse output = new GetAlarmCountResponse("99+"); + + given(getAlarmHistoryUseCase.getUnreadAlarmCount(any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform( + get("/api/history/alarm/count") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(GET_UNREAD_ALARM_HISTORY.getMessage()), + fieldWithPath("data.count").description("안 읽음 알림 개수 (99보다 크면 99+)") + + + ) + ) + ) + .andReturn(); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/infra/image/presentation/ImageControllerTest.java b/src/test/java/com/moing/backend/domain/infra/image/presentation/ImageControllerTest.java new file mode 100644 index 00000000..9465f1d9 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/infra/image/presentation/ImageControllerTest.java @@ -0,0 +1,67 @@ +package com.moing.backend.domain.infra.image.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.infra.image.application.dto.ImageFileExtension; +import com.moing.backend.domain.infra.image.application.dto.request.IssuePresignedUrlRequest; +import com.moing.backend.domain.infra.image.application.dto.response.IssuePresignedUrlResponse; +import com.moing.backend.domain.infra.image.application.service.IssuePresignedUrlUseCase; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ImageController.class) +class ImageControllerTest extends CommonControllerTest { + + @MockBean + private IssuePresignedUrlUseCase getPresignedUrlUseCase; + + @Test + public void create_presigned_url() throws Exception { + IssuePresignedUrlRequest input = IssuePresignedUrlRequest.builder() + .imageFileExtension(ImageFileExtension.JPG) + .build(); + + String body = objectMapper.writeValueAsString(input); + + IssuePresignedUrlResponse output = IssuePresignedUrlResponse.builder() + .presignedUrl("PRESIGNED_URL") + .imgUrl("IMAGE_URL") + .build(); + + + given(getPresignedUrlUseCase.execute(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + post("/api/image/presigned") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestFields( + fieldWithPath("imageFileExtension").description("이미지 확장자") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("presignedUrl을 발급하였습니다"), + fieldWithPath("data.presignedUrl").description("이미지 업로드용 PresignedUrl"), + fieldWithPath("data.imgUrl").description("업로드 후 이미지 URL") + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryTest.java b/src/test/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryTest.java new file mode 100644 index 00000000..ea725f82 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/member/domain/repository/MemberCustomRepositoryTest.java @@ -0,0 +1,35 @@ +//package com.moing.backend.domain.member.domain.repository; +// +//import com.moing.backend.domain.member.domain.entity.Member; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +//import org.springframework.transaction.annotation.Transactional; +// +//import javax.persistence.EntityManager; +// +//import java.util.List; +// +//import static org.junit.jupiter.api.Assertions.*; +//@SpringBootTest +//@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +//@ActiveProfiles("dev") +//@Transactional +//class MemberCustomRepositoryTest { +// +// @Autowired +// EntityManager em; +// +// @Autowired +// private MemberRepository memberRepository; +// @Test +// void findAllMemberOnPushAlarm() { +// List members = memberRepository.findAllMemberOnPushAlarm().orElseThrow(); +// for (Member member : members) { +// System.out.println(member.getNickName()); +// } +// +// } +//} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/mission/representation/MissionControllerTest.java b/src/test/java/com/moing/backend/domain/mission/representation/MissionControllerTest.java new file mode 100644 index 00000000..5ec529f4 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/mission/representation/MissionControllerTest.java @@ -0,0 +1,443 @@ +package com.moing.backend.domain.mission.representation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.mission.application.dto.req.MissionReq; +import com.moing.backend.domain.mission.application.dto.res.MissionConfirmRes; +import com.moing.backend.domain.mission.application.dto.res.MissionCreateRes; +import com.moing.backend.domain.mission.application.dto.res.MissionReadRes; +import com.moing.backend.domain.mission.application.service.MissionCreateUseCase; +import com.moing.backend.domain.mission.application.service.MissionDeleteUseCase; +import com.moing.backend.domain.mission.application.service.MissionReadUseCase; +import com.moing.backend.domain.mission.application.service.MissionUpdateUseCase; +import com.moing.backend.domain.mission.domain.service.MissionQueryService; +import com.moing.backend.domain.mission.presentation.MissionController; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import static com.moing.backend.domain.mission.presentation.constant.MissionResponseMessage.CONFIRM_MISSION_SUCCESS; +import static com.moing.backend.domain.mission.presentation.constant.MissionResponseMessage.RECOMMEND_MISSION_SUCCESS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.ResponseEntity.status; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +@WebMvcTest(MissionController.class) +public class MissionControllerTest extends CommonControllerTest { + + @MockBean + private MissionCreateUseCase missionCreateUseCase; + + @MockBean + private MissionUpdateUseCase missionUpdateUseCase; + + @MockBean + private MissionReadUseCase missionReadUseCase; + + @MockBean + private MissionDeleteUseCase missionDeleteUseCase; + + @MockBean + private MissionQueryService missionQueryService; + + + + @Test + public void 미션_생성() throws Exception { + //given + MissionReq input = MissionReq.builder() + .title("title") + .dueTo("2023-12-31 23:39:22.333") + .rule("rule") + .content("content") + .number(1) + .type("ONCE/REPEAT") + .way("TEXT/PHOTO/LINK") + .build(); + + String body = objectMapper.writeValueAsString(input); + + MissionCreateRes output = MissionCreateRes.builder() + .missionId(1L) + .title("title") + .dueTo("2023-12-31 23:39:22.333") + .rule("rule") + .content("content") + .number(1) + .type("ONCE/REPEAT") + .status("END/ONGOING/SUCCESS/FAIL") + .way("TEXT/PHOTO/LINK") + .isLeader(Boolean.FALSE) + .build(); + + given(missionCreateUseCase.createMission(any(),any(),any())).willReturn(output); + + Long teamId = 2L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/team/{teamId}/missions",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + requestFields( + fieldWithPath("title").description("미션 제목"), + fieldWithPath("dueTo").description("미션 마감 날짜"), + fieldWithPath("rule").description("미션 규칙"), + fieldWithPath("content").description("미션 내용"), + fieldWithPath("number").description("미션 반복 횟수"), + fieldWithPath("type").description("미션 유형(단일/반복)"), + fieldWithPath("way").description("미션 진행 방법") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.missionId").description("미션 아이디"), + fieldWithPath("data.title").description("미션 제목"), + fieldWithPath("data.dueTo").description("미션 마감 날짜"), + fieldWithPath("data.rule").description("미션 규칙"), + fieldWithPath("data.content").description("미션 내용"), + fieldWithPath("data.number").description("미션 반복 횟수"), + fieldWithPath("data.type").description("미션 유형(단일/반복)"), + fieldWithPath("data.way").description("미션 진행 방법(사진/글/링크)"), + fieldWithPath("data.status").description("미션 진행 상태"), + fieldWithPath("data.isLeader").description("미션 생성자 여부") + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_수정() throws Exception { + //given + MissionReq input = MissionReq.builder() + .title("title") + .dueTo("2023-12-31 23:39:22.333") + .rule("rule") + .content("content") + .number(1) + .type("ONCE") + .way("TEXT") + .build(); + + String body = objectMapper.writeValueAsString(input); + + MissionCreateRes output = MissionCreateRes.builder() + .missionId(1L) + .title("title") + .dueTo("2023-12-31 23:39:22.333") + .rule("rule") + .content("content") + .number(1) + .type("ONCE") + .status("END") + .way("TEXT") + .isLeader(Boolean.FALSE) + .build(); + + given(missionUpdateUseCase.updateMission(any(),any(),any())).willReturn(output); + Long teamId = 2L; + Long missionId = 2L; + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + put("/api/team/{teamId}/missions/{missionId}",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + requestFields( + fieldWithPath("title").description("미션 제목"), + fieldWithPath("dueTo").description("미션 마감 날짜"), + fieldWithPath("rule").description("미션 규칙"), + fieldWithPath("content").description("미션 내용"), + fieldWithPath("number").description("미션 반복 횟수"), + fieldWithPath("type").description("미션 타입"), + fieldWithPath("way").description("미션 진행 방법") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.missionId").description("미션 아이디"), + fieldWithPath("data.title").description("미션 제목"), + fieldWithPath("data.dueTo").description("미션 마감 날짜"), + fieldWithPath("data.rule").description("미션 규칙"), + fieldWithPath("data.content").description("미션 내용"), + fieldWithPath("data.number").description("미션 반복 횟수"), + fieldWithPath("data.type").description("미션 유형(ONCE/REPEAT)"), + fieldWithPath("data.way").description("미션 진행 방법(TEXT/PHOTO/LINK)"), + fieldWithPath("data.status").description("미션 진행 상태(END/ONGOING/SUCCESS/FAIL)"), + fieldWithPath("data.isLeader").description("미션 생성자 여부") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_조회() throws Exception { + //given + + + MissionReadRes output = MissionReadRes.builder() + .title("title") + .dueTo("2023-12-31 23:39:22.333") + .rule("rule") + .content("content") + .type("ONCE") + .way("TEXT") + .isLeader(Boolean.FALSE) + .build(); + + given(missionReadUseCase.getMission(any(),any())).willReturn(output); + + Long teamId = 2L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.title").description("미션 제목"), + fieldWithPath("data.dueTo").description("미션 마감 날짜"), + fieldWithPath("data.rule").description("미션 규칙"), + fieldWithPath("data.content").description("미션 내용"), + fieldWithPath("data.way").description("미션 진행 방법(TEXT/PHOTO/LINK)"), + fieldWithPath("data.type").description("미션 유형(ONCE/REPEAT)"), + fieldWithPath("data.isLeader").description("미션 생성자 여부") + + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_삭제() throws Exception { + //given + + Long teamId = 2L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/team/{teamId}/missions/{missionId}",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("삭제할 미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("미션을 삭제 했습니다"), + fieldWithPath("data").description("삭제된 미션 아이디") + ) + ) + ) + .andReturn(); + } + + @Test + public void 미션_추천() throws Exception { + //given + + + String output = "SPORTS/HABIT/TEST/STUDY/READING/ETC" ; + + given(missionReadUseCase.getTeamCategory(any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/recommend",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(RECOMMEND_MISSION_SUCCESS), + fieldWithPath("data").description("팀 카테고리 [SPORTS/HABIT/TEST/STUDY/READING/ETC] ") + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_종료() throws Exception { + //given + + + MissionReadRes output = MissionReadRes.builder() + .title("title") + .dueTo("2023-12-31 23:39:22.333") + .rule("rule") + .content("content") + .type("ONCE") + .way("TEXT") + .isLeader(Boolean.FALSE) + .build(); + + given(missionUpdateUseCase.terminateMissionByUser(any(),any())).willReturn(output); + + Long teamId = 2L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + put("/api/team/{teamId}/missions/{missionId}/end",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그인을 했습니다"), + fieldWithPath("data.title").description("미션 제목"), + fieldWithPath("data.dueTo").description("미션 마감 날짜"), + fieldWithPath("data.rule").description("미션 규칙"), + fieldWithPath("data.content").description("미션 내용"), + fieldWithPath("data.way").description("미션 진행 방법(TEXT/PHOTO/LINK)"), + fieldWithPath("data.type").description("미션 유형(ONCE/REPEAT)"), + fieldWithPath("data.isLeader").description("미션 생성자 여부") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_설명_확인() throws Exception { + //given + Long teamId = 2L; + Long missionId = 1L; + MissionConfirmRes output=new MissionConfirmRes(missionId); + + given(missionReadUseCase.confirmMission(any(),any(),any())).willReturn(output); + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/team/{teamId}/missions/{missionId}/confirm",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("확인 미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(CONFIRM_MISSION_SUCCESS.getMessage()), + fieldWithPath("data.missionId").description("확인한 미션의 아이디") + ) + ) + ) + .andReturn(); + } + +} diff --git a/src/test/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepositoryImplTest.java b/src/test/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepositoryImplTest.java new file mode 100644 index 00000000..e2dd262b --- /dev/null +++ b/src/test/java/com/moing/backend/domain/missionArchive/domain/repository/MissionArchiveCustomRepositoryImplTest.java @@ -0,0 +1,151 @@ +//package com.moing.backend.domain.missionArchive.domain.repository; +// +//import com.moing.backend.domain.history.application.dto.response.MemberIdAndToken; +//import com.moing.backend.domain.member.domain.entity.Member; +//import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +//import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +//import com.moing.backend.domain.mission.application.service.MissionRemindAlarmUseCase; +//import com.moing.backend.domain.mission.domain.entity.Mission; +//import com.moing.backend.domain.mission.domain.entity.constant.MissionStatus; +//import com.moing.backend.domain.mission.domain.repository.MissionRepository; +//import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +//import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchiveRes; +//import com.moing.backend.domain.missionArchive.application.dto.res.MyArchiveStatus; +//import com.moing.backend.domain.missionArchive.application.dto.res.PersonalArchiveRes; +//import com.moing.backend.domain.missionArchive.application.mapper.MissionArchiveMapper; +//import com.moing.backend.domain.missionArchive.application.service.MissionArchiveCreateUseCase; +//import com.moing.backend.domain.missionArchive.application.service.MissionArchiveReadUseCase; +//import com.moing.backend.domain.missionArchive.domain.entity.MissionArchive; +//import com.moing.backend.domain.missionArchive.domain.service.MissionArchiveScheduleQueryService; +//import com.querydsl.jpa.impl.JPAQueryFactory; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +// +//import javax.persistence.EntityManager; +//import java.util.List; +//import java.util.Optional; +// +//import static com.moing.backend.domain.mission.domain.entity.QMission.mission; +//import static com.moing.backend.domain.missionState.domain.entity.QMissionState.missionState; +//@SpringBootTest +// +//class MissionArchiveCustomRepositoryImplTest { +// +// @Autowired +// EntityManager em; +// +// @Autowired +// private MissionArchiveRepository missionArchiveRepository; +// +// @Autowired +// private MissionRepository missionRepository; +// +// @Autowired +// MissionRemindAlarmUseCase missionRemindAlarmUseCase; +// +// @Autowired +// MissionArchiveScheduleQueryService missionArchiveScheduleQueryService; +// +// @Autowired +// MissionArchiveCreateUseCase missionArchiveCreateUseCase; +// +// @Autowired +// MissionArchiveReadUseCase missionArchiveReadUseCase; +// +// +// +// +// @Test +// void findEndMission() { +// List missionByDueTo = missionRepository.findMissionByDueTo().orElseThrow(); +// for (Mission mission1 : missionByDueTo) { +// System.out.println(mission1.getTitle()+ mission1.getStatus()); +// mission1.updateStatus(MissionStatus.END); +// System.out.println(mission1.getStatus()); +// } +// } +// +// +// @Test +// void findInCompleteMission() { +// List singleMissionBoardResList = missionArchiveRepository.findSingleMissionInComplete(33L, 48L, MissionStatus.ONGOING, OrderCondition.DUETO).orElseThrow(); +// for (SingleMissionBoardRes singleMissionBoardRes : singleMissionBoardResList) { +// System.out.println(singleMissionBoardRes.getTitle()); +// } +// } +// +// @Test +// void findMissionStatus() { +// MissionArchive missionArchive = missionArchiveRepository.findById(198L).orElseThrow(); +// MissionArchiveRes missionArchiveRes = MissionArchiveMapper.mapToMissionArchiveRes(missionArchive,33L); +// System.out.println(missionArchiveRes.toString()); +// } +// +// @Test +// void createMissionArchive() { +// System.out.println(missionArchiveCreateUseCase.createArchive("KAKAO@tester02", 211L, MissionArchiveReq.builder() +// .status("SKIP") +// .archive("hihi") +// .build())); +// +// System.out.println(); +// +// +// } +// +// +// @Test +// void findRemainPeople() { +// +// List members = missionRepository.findRepeatMissionPeopleByStatus(MissionStatus.WAIT).orElseThrow(); +// for (Member member : members) { +// System.out.println(member.getNickName()); +// } } +// +//// @Test +//// void report() { +//// +//// reportCreateUseCase.createReport("KAKAO@tester02", 550L, "MISSION"); +//// +//// List teams = new ArrayList<>(); +//// teams.add(48L); +//// List top5ArchivesByTeam = missionArchiveRepository.findTop5ArchivesByTeam(teams).orElseThrow(); +//// +//// System.out.println(top5ArchivesByTeam.get(0).getPhoto().get(0).toString()); +//// +//// } +// +// +// @Test +// void catHeartList() { +// +// List personalArchive = missionArchiveReadUseCase.getPersonalArchive("APPLE@001616.4e6d97f481fa441aa3a6169666c5552a.0841", 234L); +// for (PersonalArchiveRes personalArchiveRes : personalArchive) { +// System.out.println(personalArchiveRes); +// } +// } +// +// @Test +// void finishiMissions() { +// List finishMissionBoardResList = missionArchiveRepository.findFinishMissionsByStatus(33L, 48L).orElseThrow(); +// for (FinishMissionBoardRes finishMissionBoardRes : finishMissionBoardResList) { +// System.out.println(finishMissionBoardRes.toString()); +// } +// } +// +// @Test +// void finishArchives() { +// List missionArchives = missionArchiveRepository.findMyArchives(33L, 323L).orElseThrow(); +// for (MissionArchive missionArchive : missionArchives) { +// System.out.println(missionArchive.toString()); +// } +// } +// +// @Test +// void isstatus() { +// +// MyArchiveStatus missionStatusById = missionArchiveRepository.findMissionStatusById(51L, 323L, 48L); +// System.out.println(missionStatusById); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionArchiveControllerTest.java b/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionArchiveControllerTest.java new file mode 100644 index 00000000..d05c8dd6 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionArchiveControllerTest.java @@ -0,0 +1,574 @@ +package com.moing.backend.domain.missionArchive.representation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.missionArchive.application.dto.req.MissionArchiveReq; +import com.moing.backend.domain.missionArchive.application.dto.res.*; +import com.moing.backend.domain.missionArchive.application.service.*; +import com.moing.backend.domain.missionArchive.presentation.MissionArchiveController; +import com.moing.backend.domain.missionHeart.application.dto.MissionHeartRes; +import com.moing.backend.domain.missionHeart.application.service.MissionHeartUseCase; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static com.moing.backend.domain.missionArchive.domain.constant.MissionArchiveResponseMessage.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +@WebMvcTest(MissionArchiveController.class) +public class MissionArchiveControllerTest extends CommonControllerTest { + + @MockBean + private MissionArchiveCreateUseCase missionArchiveCreateUseCase; + @MockBean + private MissionArchiveUpdateUseCase missionArchiveUpdateUseCase; + @MockBean + private MissionArchiveReadUseCase missionArchiveReadUseCase; + @MockBean + private MissionArchiveDeleteUseCase missionArchiveDeleteUseCase; + @MockBean + private RepeatMissionArchiveReadUseCase repeatMissionArchiveReadUseCase; + @MockBean + private MissionHeartUseCase missionHeartUseCase; + + + + @Test + public void 미션_인증하기() throws Exception { + //given + MissionArchiveReq input = MissionArchiveReq.builder() + .status("COMPLETE/SKIP") + .archive("content[s3 Link / text / link]") + .contents("contents") + .build(); + + String body = objectMapper.writeValueAsString(input); + + MissionArchiveRes output = MissionArchiveRes.builder() + .archiveId(1L) + .archive("content[s3 Link / text / link]") + .createdDate("2023-09-03T21:32:33.888") + .way("TEXT/LINK/PHOTO") + .status("COMPLETE/SKIP") + .count(1L) + .heartStatus("[True/False]") + .hearts(1L) + .contents("contents") + .comments(1L) + .build(); + + given(missionArchiveCreateUseCase.createArchive(any(),any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/team/{teamId}/missions/{missionId}/archive",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + requestFields( + fieldWithPath("status").description("미션 인증 상태 [COMPLETE/SKIP]"), + fieldWithPath("archive").description("미션 인증물 [s3URL/text/링크] "), + fieldWithPath("contents").description("미션 인증 문구 [null 허용] ") + + + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("미션 인증을 완료 했습니다."), + fieldWithPath("data.archiveId").description("미션 인증 아이디"), + fieldWithPath("data.archive").description("미션 인증물 [s3URL/text/링크]"), + fieldWithPath("data.way").description("미션 인증물 방식"), + fieldWithPath("data.createdDate").description("미션 제출 시각"), + fieldWithPath("data.status").description("미션 인증 상태"), + fieldWithPath("data.count").description("미션 인증 횟수"), + fieldWithPath("data.hearts").description("미션 인증 좋아요 수"), + fieldWithPath("data.heartStatus").description("미션 인증 좋아요 상태"), + fieldWithPath("data.contents").description("미션 인증 문구"), + fieldWithPath("data.comments").description("미션 댓글 개수") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_재인증하기() throws Exception { + //given + MissionArchiveReq input = MissionArchiveReq.builder() + .status("COMPLETE/SKIP") + .archive("content[s3 Link / text / link]") + .contents("contents") + .build(); + + String body = objectMapper.writeValueAsString(input); + + MissionArchiveRes output = MissionArchiveRes.builder() + .archiveId(1L) + .archive("content[s3 Link / text / link]") + .way("TEXT/LINK/PHOTO") + .createdDate("2023-09-03T21:32:33.888") + .status("COMPLETE/SKIP") + .count(1L) + .heartStatus("[True/False]") + .hearts(1L) + .contents("contents") + .comments(1L) + .build(); + + given(missionArchiveUpdateUseCase.updateArchive(any(),any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + put("/api/team/{teamId}/missions/{missionId}/archive",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + requestFields( + fieldWithPath("status").description("미션 인증 상태 [COMPLETE/SKIP]"), + fieldWithPath("archive").description("미션 인증물 [s3URL/text/링크] "), + fieldWithPath("contents").description("미션 인증 문구 [null 허용] ") + + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(UPDATE_ARCHIVE_SUCCESS), + fieldWithPath("data.archiveId").description("미션 인증 아이디"), + fieldWithPath("data.archive").description("미션 인증물 [s3URL/text/링크]"), + fieldWithPath("data.way").description("미션 인증물 방식"), + fieldWithPath("data.createdDate").description("미션 제출 시각"), + fieldWithPath("data.hearts").description("미션 인증 좋아요 수"), + fieldWithPath("data.status").description("미션 인증 상태"), + fieldWithPath("data.count").description("미션 인증 횟수"), + fieldWithPath("data.heartStatus").description("미션 인증 좋아요 상태"), + fieldWithPath("data.hearts").description("미션 인증 좋아요 수"), + fieldWithPath("data.contents").description("미션 인증 문구"), + fieldWithPath("data.comments").description("미션 댓글 개수") + + ) + ) + ) + .andReturn(); + + } +@Test + public void 미션_인증_삭제() throws Exception { + //given + + Long output = 1L; + Long count =1L; + given(missionArchiveDeleteUseCase.deleteArchive(any(),any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/team/{teamId}/missions/{missionId}/archive/{count}",teamId,missionId,count) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디"), + parameterWithName("count").description("반복미션 횟수(단일 미션일 경우 1, 반복미션일 경우 n") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(DELETE_ARCHIVE_SUCCESS), + fieldWithPath("data").description("삭제한 미션 인증 아이디(무시)") + ) + ) + ) + .andReturn(); + + } + + @Test + public void 나의_미션_인증_조회() throws Exception { + //given + + + + List archives = Lists.newArrayList(MissionArchiveRes.builder() + .archiveId(1L) + .archive("content[s3 Link / text / link]") + .way("TEXT/LINK/PHOTO") + .createdDate("2023-09-03T21:32:33.888") + .status("COMPLETE/SKIP") + .count(1L) + .heartStatus("[True/False]") + .hearts(1L) + .contents("contents") + .comments(1L) + .build()); + + MyMissionArchiveRes output = MyMissionArchiveRes.builder() + .archives(archives) + .today("True/False") + .build(); + + given(missionArchiveReadUseCase.getMyArchive(any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}/archive",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(READ_MY_ARCHIVE_SUCCESS.getMessage()), + fieldWithPath("data.today").description("오늘 인증 여부"), + fieldWithPath("data.archives[].archiveId").description("미션 인증 아이디"), + fieldWithPath("data.archives[].archive").description("미션 인증물 [s3URL/text/링크]"), + fieldWithPath("data.archives[].way").description("미션 인증물 방식"), + fieldWithPath("data.archives[].createdDate").description("미션 제출 시각"), + fieldWithPath("data.archives[].status").description("미션 인증 상태"), + fieldWithPath("data.archives[].count").description("미션 인증 횟수"), + fieldWithPath("data.archives[].heartStatus").description("미션 인증 좋아요 상태"), + fieldWithPath("data.archives[].hearts").description("미션 인증 좋아요 수"), + fieldWithPath("data.archives[].contents").description("미션 인증 문구"), + fieldWithPath("data.archives[].comments").description("미션 댓글 개수") + + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 모임원_미션_인증_조회() throws Exception { + //given + + List output = Lists.newArrayList(PersonalArchiveRes.builder() + .archiveId(1L) + .nickname("modagbul_tester1") + .profileImg("[s3 Link]") + .archive("content[s3 Link / text / link]") + .way("TEXT/LINK/PHOTO") + .createdDate("2023-09-03T21:32:33.888") + .status("COMPLETE/SKIP") + .count(1L) + .heartStatus("[True/False]") + .hearts(3) + .makerId(1L) + .contents("contents") + .comments(1L) + .build()); + + given(missionArchiveReadUseCase.getPersonalArchive(any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}/archive/others",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(READ_TEAM_ARCHIVE_SUCCESS), + fieldWithPath("data[].archiveId").description("미션 인증 아이디"), + fieldWithPath("data[].nickname").description("미션 인증자 닉네임 "), + fieldWithPath("data[].profileImg").description("미션 인증자 프로필 이미지 "), + fieldWithPath("data[].archive").description("미션 인증물 [s3URL/text/링크] "), + fieldWithPath("data[].way").description("미션 인증물 방식 "), + fieldWithPath("data[].createdDate").description("미션 인증 날짜 "), + fieldWithPath("data[].status").description("미션 인증 상태"), + fieldWithPath("data[].count").description("미션 인증 횟수"), + fieldWithPath("data[].heartStatus").description("미션 인증 좋아요 상태 "), + fieldWithPath("data[].hearts").description("미션 인증 좋아요 수 "), + fieldWithPath("data[].makerId").description("미션 인증한 사람 "), + fieldWithPath("data[].contents").description("미션 인증 문구"), + fieldWithPath("data[].comments").description("미션 인증 댓글 수") + + + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 인증_성공_인원_조회() throws Exception { + //given + + MissionArchiveStatusRes output = MissionArchiveStatusRes.builder() + .total("8") + .done("3") + .build(); + + given(missionArchiveReadUseCase.getMissionDoneStatus(any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}/archive/status",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(MISSION_ARCHIVE_PEOPLE_STATUS_SUCCESS.getMessage()), + fieldWithPath("data.total").description("전체 미션 참여자"), + fieldWithPath("data.done").description("미션 인증 완료한 미션 참여자 ") + + ) + ) + ) + .andReturn(); + + } + @Test + public void 나의_성공_횟수_조회() throws Exception { + //given + + MissionArchiveStatusRes output = MissionArchiveStatusRes.builder() + .total("8") + .done("3") + .build(); + + given(repeatMissionArchiveReadUseCase.getMyMissionDoneStatus(any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}/archive/my-status",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(MISSION_ARCHIVE_MY_STATUS_SUCCESS.getMessage()), + fieldWithPath("data.total").description("전체 미션 참여자"), + fieldWithPath("data.done").description("미션 인증 완료한 미션 참여자 ") + + ) + ) + ) + .andReturn(); + + } + @Test + public void 미션_인증물_좋아요() throws Exception { + //given + + + MissionHeartRes output = MissionHeartRes.builder() + .missionArchiveId(1L) + .missionHeartStatus("[True/False]") + .hearts(3) + .build(); + + given(missionHeartUseCase.pushHeart(any(),any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + Long archiveId = 1L; + String missionHeartStatus = "False"; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + put("/api/team/{teamId}/missions/{missionId}/archive/{archiveId}/heart/{missionHeartStatus}", teamId, missionId,archiveId,missionHeartStatus) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디"), + parameterWithName("archiveId").description("미션 인증물 아이디"), + parameterWithName("missionHeartStatus").description("미션 인증물 좋아요 상태") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(HEART_UPDATE_SUCCESS.getMessage()), + fieldWithPath("data.missionArchiveId").description("미션 인증 아이디"), + fieldWithPath("data.missionHeartStatus").description("미션 인증물 좋아요 상태 [True]"), + fieldWithPath("data.hearts").description("미션 인증물 좋아요 수") + + ) + ) + ) + .andReturn(); + } + + + @Test + public void 미션_상태_조회() throws Exception { + //given + + MyArchiveStatus output = MyArchiveStatus.builder() + .end(Boolean.TRUE) + .status("WAIT/ONGOING/COMPLETE/SKIP/FAIL") + .build(); + + given(missionArchiveReadUseCase.getMissionArchiveStatus(any(),any(),any())).willReturn(output); + + Long teamId = 1L; + Long missionId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/{missionId}/archive/mission-status",teamId,missionId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionId").description("미션 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(MISSION_ARCHIVE_PEOPLE_STATUS_SUCCESS.getMessage()), + fieldWithPath("data.end").description("미션 종료 여부"), + fieldWithPath("data.status").description("미션 인증 상태 ") + ) + ) + ) + .andReturn(); + + } + + + + + + +} diff --git a/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionBoardControllerTest.java b/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionBoardControllerTest.java new file mode 100644 index 00000000..69039960 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionBoardControllerTest.java @@ -0,0 +1,198 @@ +package com.moing.backend.domain.missionArchive.representation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.mission.application.dto.res.FinishMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.RepeatMissionBoardRes; +import com.moing.backend.domain.mission.application.dto.res.SingleMissionBoardRes; +import com.moing.backend.domain.missionArchive.application.service.MissionArchiveBoardUseCase; +import com.moing.backend.domain.missionArchive.presentation.MissionBoardController; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static com.moing.backend.domain.missionArchive.domain.constant.MissionArchiveResponseMessage.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +@WebMvcTest(MissionBoardController.class) +public class MissionBoardControllerTest extends CommonControllerTest { + + + @MockBean + private MissionArchiveBoardUseCase missionArchiveBoardUseCase; + + @Test + public void 단일_미션_인증_조회() throws Exception { + //given + + List output = Lists.newArrayList(SingleMissionBoardRes.builder() + .missionId(1L) + .dueTo("2023-09-03T21:32:33.888") + .title("Mission title") + .status("WAIT/ONGOING/SKIP/COMPLETE") + .missionType("ONCE/REPEAT") + .isRead(true) + .build()); + + given(missionArchiveBoardUseCase.getActiveSingleMissions(any(),any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/board/single",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(ACTIVE_SINGLE_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].dueTo").description("미션 마감 시각"), + fieldWithPath("data[].title").description("미션 제목"), + fieldWithPath("data[].status").description("미션 인증 상태"), + fieldWithPath("data[].missionType").description("미션 타입"), + fieldWithPath("data[].isRead").description("미션 읽음 여부") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 반복_미션_인증_조회() throws Exception { + //given + + List output = Lists.newArrayList(RepeatMissionBoardRes.builder() + .missionId(1L) + .title("Mission title") + .dueTo("True/False") + .done(1L) + .number(3) + .way("TEXT/PHOTO/LINK") + .status("WAIT/ONGOING/SKIP/COMPLETE") + .isRead(true) + .build()); + + given(missionArchiveBoardUseCase.getActiveRepeatMissions(any(),any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/board/repeat",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(ACTIVE_REPEAT_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].title").description("미션 제목"), + fieldWithPath("data[].dueTo").description("내일 리셋 상태 리턴, 일요일이면 true[True/False]"), + fieldWithPath("data[].number").description("전체 횟수"), + fieldWithPath("data[].done").description("인증한 횟수"), + fieldWithPath("data[].way").description("인증 방법"), + fieldWithPath("data[].status").description("인증 상태"), + fieldWithPath("data[].isRead").description("미션 읽음 여부") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 종료된_인증_조회() throws Exception { + //given + + List output = Lists.newArrayList(FinishMissionBoardRes.builder() + .missionId(1L) + .title("Mission title") + .dueTo("2023-09-03T21:32:33.888") + .status("SUCCESS/FAIL") + .missionType("ONCE/REPEAT") + .missionWay("TEXT/PHOTO/LINK") + .build()); + + given(missionArchiveBoardUseCase.getFinishMissions(any(),any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/missions/board/finish",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(FINISH_ALL_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].title").description("미션 제목"), + fieldWithPath("data[].dueTo").description("내일 리셋 상태 리턴, 일요일이면 true[True/False]"), + fieldWithPath("data[].status").description("미션 종료된 상태 (성공/실패)"), + fieldWithPath("data[].missionType").description("미션 타입 (한번/반복)"), + fieldWithPath("data[].missionWay").description("미션 인증 방식 ") + + ) + ) + ) + .andReturn(); + + } + + +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionGatherControllerTest.java b/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionGatherControllerTest.java new file mode 100644 index 00000000..93ff8e5d --- /dev/null +++ b/src/test/java/com/moing/backend/domain/missionArchive/representation/MissionGatherControllerTest.java @@ -0,0 +1,341 @@ +package com.moing.backend.domain.missionArchive.representation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.mission.application.dto.res.*; +import com.moing.backend.domain.mission.application.service.MissionGatherBoardUseCase; +import com.moing.backend.domain.missionArchive.application.dto.res.MissionArchivePhotoRes; +import com.moing.backend.domain.missionArchive.application.dto.res.MyTeamsRes; +import com.moing.backend.domain.missionArchive.presentation.MissionGatherController; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.ArrayList; +import java.util.List; + +import static com.moing.backend.domain.missionArchive.domain.constant.MissionArchiveResponseMessage.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; + +@WebMvcTest(MissionGatherController.class) +public class MissionGatherControllerTest extends CommonControllerTest { + + + @MockBean + private MissionGatherBoardUseCase missionGatherBoardUseCase; + + @Test + public void 미션_모아보기_단일_미션() throws Exception { + //given + + List output = Lists.newArrayList(GatherSingleMissionRes.builder() + .missionId(1L) + .teamId(1L) + .dueTo("2023-09-03T21:32:33.888") + .teamName("team name") + .missionTitle("mission title") + .status("WAIT/ONGOING/SKIP/COMPLETE") + .done("0") + .total("5") + .build()); + + given(missionGatherBoardUseCase.getAllActiveSingleMissions(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/my-once") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(ACTIVE_SINGLE_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].teamId").description("팀 아이디"), + fieldWithPath("data[].dueTo").description("미션 마감 시각"), + fieldWithPath("data[].teamName").description("팀 이름"), + fieldWithPath("data[].missionTitle").description("미션 제목"), + fieldWithPath("data[].status").description("WAIT/ONGOING=인증하지 않음 SKIP/COMPLETE=인증 완료"), + fieldWithPath("data[].done").description("미션 인증 한 인원수"), + fieldWithPath("data[].total").description("팀 전체 인원수") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 미션_모아보기_반복_미션() throws Exception { + //given + + List output = Lists.newArrayList(GatherRepeatMissionRes.builder() + .missionId(1L) + .teamId(1L) + .teamName("team name") + .missionTitle("mission title") + .doneNum("0") + .totalNum("0") + .status("WAIT/ONGOING/SKIP/COMPLETE") + .donePeople("0") + .totalPeople("0") + .build()); + + given(missionGatherBoardUseCase.getAllActiveRepeatMissions(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/my-repeat") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(ACTIVE_REPEAT_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].teamId").description("팀 아이디"), + fieldWithPath("data[].teamName").description("팀 이름"), + fieldWithPath("data[].missionTitle").description("미션 제목"), + fieldWithPath("data[].doneNum").description("내가 완료한 횟수"), + fieldWithPath("data[].totalNum").description("전체 횟수"), + fieldWithPath("data[].status").description("미션 상태"), + fieldWithPath("data[].donePeople").description("미션 완료한 인원수"), + fieldWithPath("data[].totalPeople").description("팀 전체 인원수") + + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 팀별_미션_모아보기_단일_미션() throws Exception { + //given + + List output = Lists.newArrayList(GatherSingleMissionRes.builder() + .missionId(1L) + .teamId(1L) + .dueTo("2023-09-03T21:32:33.888") + .teamName("team name") + .missionTitle("mission title") + .status("WAIT/ONGOING/SKIP/COMPLETE") + .done("0") + .total("5") + .build()); + + given(missionGatherBoardUseCase.getTeamActiveSingleMissions(any(),any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/team-once/{teamId}",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(ACTIVE_TEAM_SINGLE_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].teamId").description("팀 아이디"), + fieldWithPath("data[].dueTo").description("미션 마감 시각"), + fieldWithPath("data[].teamName").description("팀 이름"), + fieldWithPath("data[].missionTitle").description("미션 제목"), + fieldWithPath("data[].status").description("WAIT/ONGOING=인증하지 않음 SKIP/COMPLETE=인증 완료"), + fieldWithPath("data[].done").description("미션 인증 한 인원수"), + fieldWithPath("data[].total").description("팀 전체 인원수") + + ) + ) + ) + .andReturn(); + + } + + @Test + public void 팀별_미션_모아보기_반복_미션() throws Exception { + //given + + List output = Lists.newArrayList(GatherRepeatMissionRes.builder() + .missionId(1L) + .teamId(1L) + .teamName("team name") + .missionTitle("mission title") + .doneNum("0") + .totalNum("0") + .status("WAIT/ONGOING/SKIP/COMPLETE") + .donePeople("0") + .totalPeople("0") + .build()); + + given(missionGatherBoardUseCase.getTeamActiveRepeatMissions(any(),any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/team-repeat/{teamId}",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(ACTIVE_TEAM_REPEAT_MISSION_SUCCESS.getMessage()), + fieldWithPath("data[].missionId").description("미션 아이디"), + fieldWithPath("data[].teamId").description("팀 아이디"), + fieldWithPath("data[].teamName").description("팀 이름"), + fieldWithPath("data[].missionTitle").description("미션 제목"), + fieldWithPath("data[].doneNum").description("완료한 횟수"), + fieldWithPath("data[].totalNum").description("전체 횟수"), + fieldWithPath("data[].status").description("미션 상태"), + fieldWithPath("data[].donePeople").description("미션 완료한 인원수"), + fieldWithPath("data[].totalPeople").description("팀 전체 인원수") + + + ) + ) + ) + .andReturn(); + + } + @Test + public void 미션_모아보기_팀별() throws Exception { + //given + + List objects = new ArrayList<>(); + objects.add("1"); + objects.add("2"); + + List output = Lists.newArrayList(MissionArchivePhotoRes.builder() + .teamId(1L) + .photo(objects) + .build()); + + given(missionGatherBoardUseCase.getArchivePhotoByTeamRes(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/my-teams") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(MISSION_ARCHIVE_BY_TEAM.getMessage()), + fieldWithPath("data[].teamId").description("팀 아이디"), + fieldWithPath("data[].photo[]").description("팀별 미션 인증물 사진들") + ) + ) + ) + .andReturn(); + + } + + @Test + public void 내가_속한_팀_조회() throws Exception { + //given + + + List output = Lists.newArrayList(MyTeamsRes.builder() + .teamId(1L) + .teamName("teamname") + .build()); + + given(missionGatherBoardUseCase.getMyTeams(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/my-teamList") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(MISSION_ARCHIVE_BY_TEAM.getMessage()), + fieldWithPath("data[].teamId").description("팀 아이디"), + fieldWithPath("data[].teamName").description("팀 이름") + ) + ) + ) + .andReturn(); + + } + + + +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/missionComment/domain/MissionRepositoryTest.java b/src/test/java/com/moing/backend/domain/missionComment/domain/MissionRepositoryTest.java new file mode 100644 index 00000000..ee4c9b6c --- /dev/null +++ b/src/test/java/com/moing/backend/domain/missionComment/domain/MissionRepositoryTest.java @@ -0,0 +1,22 @@ +package com.moing.backend.domain.missionComment.domain; + +import com.moing.backend.domain.missionComment.domain.repository.MissionCommentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("dev") +@Transactional +public class MissionRepositoryTest { + + @Autowired + MissionCommentRepository missionCommentRepository; + public void create_Mission_Comment(){ + + } + +} diff --git a/src/test/java/com/moing/backend/domain/missionComment/presentation/MissionCommentControllerTest.java b/src/test/java/com/moing/backend/domain/missionComment/presentation/MissionCommentControllerTest.java new file mode 100644 index 00000000..072bc3eb --- /dev/null +++ b/src/test/java/com/moing/backend/domain/missionComment/presentation/MissionCommentControllerTest.java @@ -0,0 +1,192 @@ +package com.moing.backend.domain.missionComment.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.comment.application.dto.request.CreateCommentRequest; +import com.moing.backend.domain.comment.application.dto.response.CommentBlocks; +import com.moing.backend.domain.comment.application.dto.response.CreateCommentResponse; +import com.moing.backend.domain.comment.application.dto.response.GetCommentResponse; +import com.moing.backend.domain.missionComment.application.service.CreateMissionCommentUseCase; +import com.moing.backend.domain.missionComment.application.service.DeleteMissionCommentUseCase; +import com.moing.backend.domain.missionComment.application.service.GetMissionCommentUseCase; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MissionCommentController.class) +public class MissionCommentControllerTest extends CommonControllerTest { + + @MockBean + private CreateMissionCommentUseCase createMissionCommentUseCase; + @MockBean + private DeleteMissionCommentUseCase deleteMissionCommentUseCase; + @MockBean + private GetMissionCommentUseCase getMissionCommentUseCase; + + @Test + public void create_mission_comment() throws Exception { + + //given + Long teamId = 1L; + Long missionArchiveId = 1L; + CreateCommentRequest input = CreateCommentRequest.builder() + .content("게시글 내용") + .build(); + + String body = objectMapper.writeValueAsString(input); + + CreateCommentResponse output = CreateCommentResponse.builder() + .commentId(1L) + .build(); + + given(createMissionCommentUseCase.createBoardComment(any(), any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/{teamId}/{missionArchiveId}/mcomment", teamId, missionArchiveId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionArchiveId").description("미션 게시물 아이디") + ), + requestFields( + fieldWithPath("content").description("댓글 내용") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("댓글을 생성했습니다"), + fieldWithPath("data.commentId").description("생성한 boardCommentId") + ) + ) + ); + } + + @Test + public void delete_mission_comment() throws Exception { + + //given + Long teamId = 1L; + Long boardId = 1L; + Long missionArchiveId = 1L; + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/{teamId}/{missionArchiveId}/mcomment/{boardCommentId}", teamId, boardId, missionArchiveId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionArchiveId").description("미션 게시글 아이디"), + parameterWithName("boardCommentId").description("댓글 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("댓글을 삭제했습니다") + ) + ) + ); + } + + + @Test + public void get_board_comment_all() throws Exception { + //given + List commentBlocks = new ArrayList<>(); + Long teamId = 1L; + Long boardId = 1L; + + CommentBlocks commentBlock = CommentBlocks.builder() + .commentId(1L) + .content("댓글 내용") + .writerIsLeader(true) + .writerNickName("작성자 닉네임") + .writerProfileImage("작성자 프로필 이미지") + .writerIsDeleted(false) + .isWriter(true) + .createdDate("2023/12/05 23:29") + .makerId(1L) + .build(); + + commentBlocks.add(commentBlock); + + GetCommentResponse output = new GetCommentResponse(commentBlocks); + + given(getMissionCommentUseCase.getBoardCommentAll(any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/{teamId}/{missionArchiveId}/mcomment", teamId, boardId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디"), + parameterWithName("missionArchiveId").description("게시글 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("댓글 목록을 모두 조회했습니다."), + fieldWithPath("data.commentBlocks[].commentId").description("댓글 아이디"), + fieldWithPath("data.commentBlocks[].content").description("댓글 내용"), + fieldWithPath("data.commentBlocks[].writerIsLeader").description("작성자 소모임장 여부"), + fieldWithPath("data.commentBlocks[].writerNickName").description("작성자 닉네임"), + fieldWithPath("data.commentBlocks[].writerProfileImage").description("작성자 프로필 이미지"), + fieldWithPath("data.commentBlocks[].writerIsDeleted").description("작성자 삭제 여부"), + fieldWithPath("data.commentBlocks[].isWriter").description("댓글 작성자 여부"), + fieldWithPath("data.commentBlocks[].createdDate").description("생성 시간"), + fieldWithPath("data.commentBlocks[].makerId").description("작성자 Id") + ) + + ) + ); + } +} diff --git a/src/test/java/com/moing/backend/domain/missionState/domain/service/MissionStateQueryServiceTest.java b/src/test/java/com/moing/backend/domain/missionState/domain/service/MissionStateQueryServiceTest.java new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/com/moing/backend/domain/mypage/presentation/MypageControllerTest.java b/src/test/java/com/moing/backend/domain/mypage/presentation/MypageControllerTest.java new file mode 100644 index 00000000..d2ea767c --- /dev/null +++ b/src/test/java/com/moing/backend/domain/mypage/presentation/MypageControllerTest.java @@ -0,0 +1,339 @@ +package com.moing.backend.domain.mypage.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.mypage.application.dto.request.UpdateProfileRequest; +import com.moing.backend.domain.mypage.application.dto.request.WithdrawRequest; +import com.moing.backend.domain.mypage.application.dto.response.GetAlarmResponse; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageResponse; +import com.moing.backend.domain.mypage.application.dto.response.GetMyPageTeamBlock; +import com.moing.backend.domain.mypage.application.dto.response.GetProfileResponse; +import com.moing.backend.domain.mypage.application.service.*; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MyPageController.class) +public class MypageControllerTest extends CommonControllerTest { + @MockBean + private SignOutUseCase signOutService; + + @MockBean + private WithdrawUseCase withdrawService; + + @MockBean + private ProfileUseCase profileUseCase; + + @MockBean + private AlarmUseCase alarmUseCase; + + @MockBean + private GetMyPageUseCase getMyPageUseCase; + + @Test + public void sign_out() throws Exception { + //when + ResultActions actions = mockMvc.perform( + post("/api/mypage/signOut") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("로그아웃을 했습니다") + ) + ) + ); + } + + @Test + public void withdraw() throws Exception { + //given + WithdrawRequest input = WithdrawRequest.builder() + .reason("REASON_TO_LEAVE") + .socialToken("SOCIAL_TOKEN") + .build(); + + String body = objectMapper.writeValueAsString(input); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/mypage/withdrawal/{provider}","google,kakao,apple") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("provider").description("google, kakao, apple") + ), + requestFields( + fieldWithPath("socialToken").description("google:accessToken/ kakao:accessToken/ apple:authorization code"), + fieldWithPath("reason").description("회원탈퇴하는 이유") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("회원탈퇴를 했습니다") + ) + ) + ); + } + + @Test + public void get_mypage() throws Exception{ + //given + List categoryList=new ArrayList<>(); + categoryList.add("SPORTS"); + + List getMyPageTeamBlocks=new ArrayList<>(); + GetMyPageTeamBlock blocks= GetMyPageTeamBlock.builder() + .teamId(1L) + .teamName("소모임이름") + .category("SPORTS") + .profileImgUrl("프로필 이미지 url") + .build(); + getMyPageTeamBlocks.add(blocks); + + GetMyPageResponse output = GetMyPageResponse.builder() + .profileImage("PROFILE_IMAGE_URL") + .introduction("INTRODUCTION") + .nickName("NICKNAME") + .categories(categoryList) + .getMyPageTeamBlocks(getMyPageTeamBlocks) + .build(); + + given(getMyPageUseCase.getMyPageResponse(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + get("/api/mypage") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("마이페이지를 조회했습니다"), + fieldWithPath("data.profileImage").description("프로필 이미지 URL"), + fieldWithPath("data.nickName").description("닉네임"), + fieldWithPath("data.introduction").description("한줄 소개"), + fieldWithPath("data.categories[]").description("내 열정의 불 해시태그"), + fieldWithPath("data.getMyPageTeamBlocks[].teamId").description("소모임 아이디"), + fieldWithPath("data.getMyPageTeamBlocks[].teamName").description("소모임 이름"), + fieldWithPath("data.getMyPageTeamBlocks[].category").description("소모임 카테고리"), + fieldWithPath("data.getMyPageTeamBlocks[].profileImgUrl").description("소모임 프로필 이미지 URL") + ) + ) + ); + } + + @Test + public void get_profile() throws Exception { + //given + GetProfileResponse output = GetProfileResponse.builder() + .profileImage("PROFILE_IMAGE_URL") + .introduction("INTRODUCTION") + .nickName("NICKNAME") + .build(); + + given(profileUseCase.getProfile(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + get("/api/mypage/profile") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("프로필을 조회했습니다"), + fieldWithPath("data.profileImage").description("프로필 이미지 URL"), + fieldWithPath("data.nickName").description("닉네임"), + fieldWithPath("data.introduction").description("한줄 소개") + ) + ) + ); + } + + @Test + public void update_profile() throws Exception { + //given + UpdateProfileRequest input = UpdateProfileRequest.builder() + .profileImage("PROFILE_IMAGE_URL") + .introduction("INTRODUCTION") + .nickName("NICKNAME") + .build(); + + String body = objectMapper.writeValueAsString(input); + + //when + ResultActions actions = mockMvc.perform( + put("/api/mypage/profile") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + requestFields( + fieldWithPath("profileImage").description("프로필 이미지 URL"), + fieldWithPath("nickName").description("닉네임"), + fieldWithPath("introduction").description("한줄 소개") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("프로필을 수정했습니다.") + ) + ) + ); + } + + + @Test + public void get_alarm() throws Exception { + //given + GetAlarmResponse output = GetAlarmResponse.builder() + .isNewUploadPush(true) + .isFirePush(true) + .isRemindPush(true) + .isCommentPush(true) + .build(); + + given(alarmUseCase.getAlarm(any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + get("/api/mypage/alarm") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("알람 정보를 조회했습니다"), + fieldWithPath("data.newUploadPush").description("신규 공지 알림"), + fieldWithPath("data.remindPush").description("미션 리마인드 알림"), + fieldWithPath("data.firePush").description("불 던지기 알림"), + fieldWithPath("data.commentPush").description("댓글 알림") + ) + ) + ); + } + + @Test + public void update_alarm() throws Exception { + //given + GetAlarmResponse output = GetAlarmResponse.builder() + .isNewUploadPush(true) + .isFirePush(true) + .isRemindPush(true) + .isCommentPush(true) + .build(); + + given(alarmUseCase.updateAlarm(any(),any(),any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform( + put("/api/mypage/alarm") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .param("type", "all") // 파라미터 추가 + .param("status", "on") // 파라미터 추가 + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + requestParameters( // 요청 파라미터 문서화 + parameterWithName("type").description("all || isNewUploadPush || isRemindPush || isFirePush"), + parameterWithName("status").description("on || off") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("알람 정보를 수정했습니다"), + fieldWithPath("data.newUploadPush").description("신규 공지 알림"), + fieldWithPath("data.remindPush").description("미션 리마인드 알림"), + fieldWithPath("data.firePush").description("불 던지기 알림"), + fieldWithPath("data.commentPush").description("댓글 알림") + + ) + ) + ); + } +} diff --git a/src/test/java/com/moing/backend/domain/report/presentation/ReportControllerTest.java b/src/test/java/com/moing/backend/domain/report/presentation/ReportControllerTest.java new file mode 100644 index 00000000..93e1953d --- /dev/null +++ b/src/test/java/com/moing/backend/domain/report/presentation/ReportControllerTest.java @@ -0,0 +1,73 @@ +package com.moing.backend.domain.report.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.report.application.service.ReportCreateUseCase; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import static com.moing.backend.domain.report.presentation.constant.ReportResponseMessage.CREATE_REPORT_SUCCESS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +@WebMvcTest(ReportController.class) +public class ReportControllerTest extends CommonControllerTest { + + @MockBean + private ReportCreateUseCase reportCreateUseCase; + + @Test + public void 신고하기() throws Exception { + //given + + Long targetId = 1L; + + given(reportCreateUseCase.createReport(any(),any(),any())).willReturn(targetId); + + + String reportType = "MISSION"; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/report/{reportType}/{targetId}", reportType, targetId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + pathParameters( + parameterWithName("reportType").description("MISSION/BOARD/BCOMMENT/MCOMMENT"), + parameterWithName("targetId").description("신고할 board,missionArchive,comment 아이디") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(CREATE_REPORT_SUCCESS.getMessage()), + fieldWithPath("data").description("신고한 게시글 번호") + + ) + ) + ) + .andReturn(); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/team/application/service/CreateTeamUseCaseTest.java b/src/test/java/com/moing/backend/domain/team/application/service/CreateTeamUseCaseTest.java new file mode 100644 index 00000000..e38801c2 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/team/application/service/CreateTeamUseCaseTest.java @@ -0,0 +1,12 @@ +package com.moing.backend.domain.team.application.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CreateTeamUseCaseTest { + + @Test + void createTeam() { + } +} \ No newline at end of file diff --git a/src/test/java/com/moing/backend/domain/team/presentation/TeamControllerTest.java b/src/test/java/com/moing/backend/domain/team/presentation/TeamControllerTest.java new file mode 100644 index 00000000..93a3fb28 --- /dev/null +++ b/src/test/java/com/moing/backend/domain/team/presentation/TeamControllerTest.java @@ -0,0 +1,570 @@ +package com.moing.backend.domain.team.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.member.domain.constant.Gender; +import com.moing.backend.domain.member.dto.response.UserProperty; +import com.moing.backend.domain.team.application.dto.request.CreateTeamRequest; +import com.moing.backend.domain.team.application.dto.request.UpdateTeamRequest; +import com.moing.backend.domain.team.application.dto.response.*; +import com.moing.backend.domain.team.application.service.*; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(TeamController.class) +public class TeamControllerTest extends CommonControllerTest { + @MockBean + private CreateTeamUseCase createTeamService; + @MockBean + private GetTeamUseCase getTeamUseCase; + @MockBean + private DisbandTeamUseCase disbandTeamUseCase; + @MockBean + private WithdrawTeamUseCase withdrawTeamUseCase; + @MockBean + private SignInTeamUseCase signInTeamUseCase; + @MockBean + private UpdateTeamUseCase updateTeamUseCase; + @MockBean + private ReviewTeamUseCase reviewTeamUseCase; + + @Test + public void create_team() throws Exception { + + //given + CreateTeamRequest input = CreateTeamRequest.builder() + .category("ETC") + .name("소모임 이름") + .introduction("소모임 소개글") + .profileImgUrl("소모임 대표 사진 URL") + .promise("소모임 각오") + .build(); + + String body = objectMapper.writeValueAsString(input); + + CreateTeamResponse output = CreateTeamResponse.builder() + .teamId(1L) + .build(); + + given(createTeamService.createTeam(any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform( + post("/api/team") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + requestFields( + fieldWithPath("category").description("카테고리"), + fieldWithPath("name").description("소모임 이름"), + fieldWithPath("introduction").description("소모임 소개글"), + fieldWithPath("profileImgUrl").description("소모임 대표 사진"), + fieldWithPath("promise").description("소모임 각오") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("소모임을 생성하였습니다"), + fieldWithPath("data.teamId").description("생성한 teamId") + ) + ) + ); + } + @Test + public void get_team() throws Exception { + //given + List teamBlocks=new ArrayList<>(); + + TeamBlock teamBlock1=TeamBlock.builder() + .teamId(1L) + .duration(5L) + .levelOfFire(3) + .teamName("소모임 예시1") + .numOfMember(10) + .category("ETC") + .startDate("2023.09.05") + .deletionTime(LocalDateTime.now().withNano(0)) + .profileImgUrl("프로필 사진 url") + .build(); + + TeamBlock teamBlock2=TeamBlock.builder() + .teamId(2L) + .duration(10L) + .levelOfFire(3) + .teamName("소모임 예시2") + .numOfMember(8) + .category("SPORTS") + .startDate("2023.09.01") + .deletionTime(LocalDateTime.now().withNano(0)) + .profileImgUrl("프로필 사진 url") + .build(); + + teamBlocks.add(teamBlock1); + teamBlocks.add(teamBlock2); + + UserProperty userProperty=new UserProperty(Gender.WOMAN, LocalDate.of(2000, 3, 28)); + GetTeamResponse output = GetTeamResponse.builder() + .numOfTeam(1) + .memberNickName("유저 닉네임") + .teamBlocks(teamBlocks) + .userProperty(userProperty) + .build(); + + given(getTeamUseCase.getTeam(any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform( + get("/api/team") + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("홈 화면에서 내 소모임을 모두 조회했습니다."), + fieldWithPath("data.numOfTeam").description("소모임 개수(최대 3개)"), + fieldWithPath("data.memberNickName").description("유저 닉네임"), + fieldWithPath("data.teamBlocks[].teamId").description("소모임 아이디"), + fieldWithPath("data.teamBlocks[].duration").description("소모임과 함께한 시간"), + fieldWithPath("data.teamBlocks[].levelOfFire").description("불꽃 레벨"), + fieldWithPath("data.teamBlocks[].teamName").description("소모임 이름"), + fieldWithPath("data.teamBlocks[].numOfMember").description("소모임원 명 수"), + fieldWithPath("data.teamBlocks[].category").description("소모임 카테고리"), + fieldWithPath("data.teamBlocks[].startDate").description("소모임 시작일"), + fieldWithPath("data.teamBlocks[].deletionTime").description("소모임 삭제 시간 (삭제 안했으면 null)"), + fieldWithPath("data.teamBlocks[].profileImgUrl").description("프로필 사진 url"), + fieldWithPath("data.userProperty.gender").description("유저 성별"), + fieldWithPath("data.userProperty.birthDate").description("유저 태어난 날") + ) + + ) + ); + } + + @Test + public void get_team_detail() throws Exception { + //given + Long teamId = 1L; + List teamMemberInfoList = new ArrayList<>(); + TeamMemberInfo teamMemberInfo = TeamMemberInfo.builder() + .memberId(1L) + .nickName("소모임원 닉네임") + .profileImage("소모임원 프로필 사진") + .introduction("소모임원 소개") + .isLeader(true) + .build(); + teamMemberInfoList.add(teamMemberInfo); + + TeamInfo teamInfo = TeamInfo + .builder() + .isDeleted(true) + .deletionTime(LocalDateTime.now()) + .teamName("소모임 이름") + .numOfMember(1) + .category("ETC") + .introduction("소모임 소개글") + .currentUserId(1L) + .teamMemberInfoList(teamMemberInfoList).build(); + + GetTeamDetailResponse output = GetTeamDetailResponse + .builder() + .boardNum(1) + .teamInfo(teamInfo) + .build(); + + + given(getTeamUseCase.getTeamDetailResponse(any(),any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/board/{teamId}", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("목표보드를 조회했습니다."), + fieldWithPath("data.boardNum").description("안 읽은 공지/게시글"), + fieldWithPath("data.teamInfo.teamName").description("소모임 이름"), + fieldWithPath("data.teamInfo.numOfMember").description("모임원 명 수"), + fieldWithPath("data.teamInfo.category").description("카테고리"), + fieldWithPath("data.teamInfo.introduction").description("모임 소개"), + fieldWithPath("data.teamInfo.isDeleted").description("삭제여부"), + fieldWithPath("data.teamInfo.deletionTime").description("삭제 시간 (삭제 안했으면 null)"), + fieldWithPath("data.teamInfo.currentUserId").description("현재 유저 아이디"), + fieldWithPath("data.teamInfo.teamMemberInfoList[0].memberId").description("유저 아이디"), + fieldWithPath("data.teamInfo.teamMemberInfoList[0].nickName").description("유저 닉네임"), + fieldWithPath("data.teamInfo.teamMemberInfoList[0].profileImage").description("유저 프로필 이미지"), + fieldWithPath("data.teamInfo.teamMemberInfoList[0].introduction").description("유저 소개"), + fieldWithPath("data.teamInfo.teamMemberInfoList[0].isLeader").description("유저 소모임장 여부") + ) + + ) + ); + } + + @Test + public void review_team() throws Exception { + //given + Long teamId = 1L; // 예시 ID + ReviewTeamResponse output = ReviewTeamResponse.builder() + .teamId(teamId) + .teamName("팀 이름") + .numOfMember(9) + .duration(30L) + .numOfMission(90L) + .levelOfFire(3) + .isLeader(false) + .memberName("유저 닉네임") + .build(); + + given(reviewTeamUseCase.reviewTeam(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/review", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("소모임 삭제 전 조회했습니다."), + fieldWithPath("data.teamId").description("삭제할 소모임 id"), + fieldWithPath("data.teamName").description("소모임 이름"), + fieldWithPath("data.numOfMember").description("모임원 명 수"), + fieldWithPath("data.duration").description("소모임과 함께한 시간"), + fieldWithPath("data.levelOfFire").description("소모임 불꽃 레벨"), + fieldWithPath("data.numOfMission").description("미션 총 개수"), + fieldWithPath("data.isLeader").description("소모임장 여부"), + fieldWithPath("data.memberName").description("유저 닉네임") + ) + ) + ); + } + + @Test + public void disband_team() throws Exception { + //given + Long teamId = 1L; // 예시 ID + DeleteTeamResponse output = DeleteTeamResponse.builder() + .teamId(teamId) + .build(); + + given(disbandTeamUseCase.disbandTeam(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/team/{teamId}/disband", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("[소모임장 권한] 소모임을 강제 종료했습니다."), + fieldWithPath("data.teamId").description("강제종료한 소모임 id") + ) + ) + ); + } + @Test + public void withdraw_team() throws Exception { + //given + Long teamId = 1L; // 예시 ID + DeleteTeamResponse output = DeleteTeamResponse.builder() + .teamId(teamId) + .build(); + + given(withdrawTeamUseCase.withdrawTeam(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + delete("/api/team/{teamId}/withdraw", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("[소모임원 권한] 소모임을 탈퇴하였습니다"), + fieldWithPath("data.teamId").description("탈퇴한 소모임 id") + ) + ) + ); + } + + @Test + public void signIn_team() throws Exception { + //given + Long teamId = 1L; // 예시 ID + CreateTeamResponse output = CreateTeamResponse.builder() + .teamId(teamId) + .build(); + + given(signInTeamUseCase.signInTeam(any(), any())).willReturn(output); + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + post("/api/team/{teamId}", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("소모임에 가입하였습니다."), + fieldWithPath("data.teamId").description("가입한 소모임 id") + ) + ) + ); + } + + + + @Test + public void update_team() throws Exception { + + // given + Long teamId = 1L; + UpdateTeamRequest input = UpdateTeamRequest.builder() + .name("수정 후 팀 이름") + .introduction("수정 후 소개") + .profileImgUrl("수정 후 프로필 이미지") + .build(); + + + String body = objectMapper.writeValueAsString(input); + + UpdateTeamResponse output = UpdateTeamResponse.builder() + .teamId(1L) + .build(); + + given(updateTeamUseCase.updateTeam(any(), any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + put("/api/team/{teamId}", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + requestFields( + fieldWithPath("name").description("변경 후 소모임 이름"), + fieldWithPath("introduction").description("변경 후 소모임 소개글"), + fieldWithPath("profileImgUrl").description("변경 후 소모임 대표 사진") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("소모임을 수정했습니다."), + fieldWithPath("data.teamId").description("수정한 teamId") + ) + ) + ); + } + + @Test + public void get_current_status() throws Exception { + + // given + Long teamId = 1L; + + GetCurrentStatusResponse output = GetCurrentStatusResponse.builder() + .name("팀 이름") + .introduction("소개") + .profileImgUrl("프로필 이미지") + .build(); + + given(getTeamUseCase.getCurrentStatus(any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("소모임을 수정했습니다."), + fieldWithPath("data.name").description("소모임 이름"), + fieldWithPath("data.introduction").description("소모임 소개글"), + fieldWithPath("data.profileImgUrl").description("소모임 대표 사진") + ) + ) + ); + } + + @Test + public void get_team_count() throws Exception { + + // given + Long teamId = 1L; + + GetTeamCountResponse output = GetTeamCountResponse.builder() + .teamName("소모임 이름") + .numOfTeam(2L) + .leaderName("소모임장 이름") + .memberName("멤버 이름") + .build(); + + given(getTeamUseCase.getTeamCount(any(), any())).willReturn(output); + + + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/count", teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + ); + + + //then + actions + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + pathParameters( + parameterWithName("teamId").description("팀 아이디") + ), + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description("소모임을 수정했습니다."), + fieldWithPath("data.teamName").description("소모임 이름"), + fieldWithPath("data.numOfTeam").description("지금까지 가입한 소모임 개수"), + fieldWithPath("data.leaderName").description("소모임장 이름"), + fieldWithPath("data.memberName").description("유저 닉네임") + ) + ) + ); + } + +} diff --git a/src/test/java/com/moing/backend/domain/teamScore/presentation/TeamScoreControllerTest.java b/src/test/java/com/moing/backend/domain/teamScore/presentation/TeamScoreControllerTest.java new file mode 100644 index 00000000..6782317e --- /dev/null +++ b/src/test/java/com/moing/backend/domain/teamScore/presentation/TeamScoreControllerTest.java @@ -0,0 +1,73 @@ +package com.moing.backend.domain.teamScore.presentation; + +import com.moing.backend.config.CommonControllerTest; +import com.moing.backend.domain.teamScore.application.dto.TeamScoreRes; +import com.moing.backend.domain.teamScore.application.service.TeamScoreGetUseCase; +import com.moing.backend.domain.teamScore.application.service.TeamScoreUpdateUseCase; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import static com.moing.backend.domain.teamScore.presentation.constant.TeamScoreResponseMessage.GET_TEAMSCORE_SUCCESS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; + +@WebMvcTest(TeamScoreController.class) +public class TeamScoreControllerTest extends CommonControllerTest { + + + @MockBean + private TeamScoreGetUseCase teamScoreGetUseCase; + + @Test + public void 팀별_불_레벨_경험치_조회() throws Exception { + //given + + TeamScoreRes output = TeamScoreRes.builder() + .score(1L) + .level(1L) + .build(); + + given(teamScoreGetUseCase.getTeamScoreInfo(any())).willReturn(output); + + Long teamId = 1L; + //when + ResultActions actions = mockMvc.perform(RestDocumentationRequestBuilders. + get("/api/team/{teamId}/my-fire",teamId) + .header("Authorization", "Bearer ACCESS_TOKEN") + .contentType(MediaType.APPLICATION_JSON) + + ); + + //then + actions + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo( + restDocs.document( + requestHeaders( + headerWithName("Authorization").description("접근 토큰") + ), + + responseFields( + fieldWithPath("isSuccess").description("true"), + fieldWithPath("message").description(GET_TEAMSCORE_SUCCESS.getMessage()), + fieldWithPath("data.score").description("팀 경험치"), + fieldWithPath("data.level").description("팀 불 레벨") + + ) + ) + ) + .andReturn(); + + } + + +} \ No newline at end of file