diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 37de4f31..02a675f4 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -1,96 +1,66 @@ -# 워크플로우의 이름 지정 name: ASAP-DEV-CD -# 해당 workflow가 언제 실행될 것인지에 대한 트리거를 지정 on: push: - branches: [ "develop" ] # main branch로 push 될 때 실행됩니다. - -env: - S3_BUCKET_NAME: asap-develop-bucket + branches: [ "develop" ] jobs: build: name: Code deployment - # 실행 환경 runs-on: ubuntu-latest steps: - - # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요 - - name: checkout - uses: actions/checkout@v3 - - # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - # 3) 환경변수 파일 생성 - - name: make application.properties 파일 생성 - run: | - ## create application.yml - cd ./src/main/resources + - name: checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: make application.yml 파일 생성 + run: | + ## create application.yml + cd ./src/main/resources + + # application.yml 파일 생성 + touch ./application.yml + + # GitHub-Actions 에서 설정한 값을 application.yml 파일에 쓰기 + echo "${{ secrets.DOCKER_DEV_YAML }}" >> ./application.yml + + # application.yml 파일 확인 + cat ./application.yml + shell: bash + + - name: Build with Gradle + run: | + chmod +x gradlew + ./gradlew build -x test + + - name: docker build 가능하도록 환경 설정 + uses: docker/setup-buildx-action@v2.9.1 + + - name: docker hub에로그인 + uses: docker/login-action@v2.2.0 + with: + username: ${{ secrets.DOCKERHUB_LOGIN_USERNAME }} + password: ${{ secrets.DOCKERHUB_LOGIN_ACCESSTOKEN }} - # application.yml 파일 생성 - touch ./application.yml + - name: docker image 빌드 및 푸시 + run: | + docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE_NAME }} . + docker tag ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE_NAME }} ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE_NAME }}:${{ secrets.DOCKERHUB_DEV_IMAGE_NAME }} + docker push ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE_NAME }}:${{ secrets.DOCKERHUB_DEV_IMAGE_NAME }} - # GitHub-Actions 에서 설정한 값을 application.yml 파일에 쓰기 - echo "${{ secrets.DEV_APPLICATION_YAML }}" >> ./application.yml - - # application.yml 파일 확인 - cat ./application.yml - shell: bash - - # 이 워크플로우는 gradle build - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with Gradle # 실제 application build 테스트 제외 - run: ./gradlew build -x test - -# # 디렉토리 생성 -# - name: Make Directory -# run: mkdir -p deploy -# -# # Jar 파일 복사 -# - name: Copy Jar -# run: cp ./build/libs/*.jar ./deploy -# -# # appspec.yml 파일 복사 -# - name: Copy appspec.yml -# run: cp appspec.yml ./deploy -# -# # script files 복사 -# - name: Copy script -# run: cp ./scripts/*.sh ./deploy -# -# - name: Make zip file -# run: zip -r ./asap_dev_server.zip ./deploy -# shell: bash -# -# - name: Configure AWS credentials -# uses: aws-actions/configure-aws-credentials@v1 -# with: -# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} -# aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} -# aws-region: ap-northeast-2 -# -# - name: Upload to S3 -# run: aws s3 cp --region ap-northeast-2 ./asap_dev_server.zip s3://$S3_BUCKET_NAME/ - -# # Deploy -# - name: Deploy -# env: -# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }} -# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }} -# run: -# aws deploy create-deployment -# --application-name asap-dev-codedeploy -# --deployment-group-name asap-dev-codedeploy-group -# --file-exists-behavior OVERWRITE -# --s3-location bucket=asap-develop-bucket,bundleType=zip,key=asap_dev_server.zip -# --region ap-northeast-2 + - name: 도커 컨테이너 실행 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.DEV_SERVER_IP }} + username: ${{ secrets.DEV_SERVER_USER }} + key: ${{ secrets.DEV_SERVER_KEY }} + script: | + cd ~ + sudo ./deploy.sh \ No newline at end of file diff --git a/HELP.md b/HELP.md deleted file mode 100644 index 7d8d1f59..00000000 --- a/HELP.md +++ /dev/null @@ -1,24 +0,0 @@ -# Getting Started - -### Reference Documentation -For further reference, please consider the following sections: - -* [Official Gradle documentation](https://docs.gradle.org) -* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.7.13/gradle-plugin/reference/html/) -* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.7.13/gradle-plugin/reference/html/#build-image) -* [Spring Web](https://docs.spring.io/spring-boot/docs/2.7.13/reference/htmlsingle/#web) -* [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/2.7.13/reference/htmlsingle/#actuator) - -### Guides -The following guides illustrate how to use some features concretely: - -* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) -* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) -* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) -* [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) - -### Additional Links -These additional references should also help you: - -* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) - diff --git a/appspec.yml b/appspec.yml deleted file mode 100644 index b42d8475..00000000 --- a/appspec.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: 0.0 -os: linux - -files: - - source: / - destination: /home/ubuntu/app - overwrite: yes - -permissions: - - object: / - pattern: "**" - owner: ubuntu - group: ubuntu - -hooks: - AfterInstall: - - location: deploy.sh - timeout: 180 - runas: ubuntu diff --git a/build.gradle b/build.gradle index c789db67..14c6c556 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'com.asap' -version = '1.2.2' +version = '1.3.0' java { sourceCompatibility = '17' @@ -19,6 +19,12 @@ bootJar { enabled = true; } +tasks.test { + testLogging { + exceptionFormat = 'full' + } +} + configurations { compileOnly { extendsFrom annotationProcessor @@ -62,7 +68,7 @@ dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' // Slack Webhook - implementation 'com.slack.api:slack-api-client:1.28.0' + implementation 'com.slack.api:slack-api-client:1.30.0' implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.slack.api:slack-app-backend:1.28.0' diff --git a/scripts/deploy.sh b/scripts/deploy.sh deleted file mode 100644 index 14ffa689..00000000 --- a/scripts/deploy.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash -BUILD_PATH=$(ls /home/ubuntu/app/server-0.0.1-SNAPSHOT.jar) -JAR_NAME=$(basename $BUILD_PATH) -echo "> build 파일명: $JAR_NAME" - -echo "> build 파일 복사" -DEPLOY_PATH=/home/ubuntu/app/nonstop/jar/ -cp $BUILD_PATH $DEPLOY_PATH - -echo "> 현재 구동중인 Set 확인" -CURRENT_PROFILE=$(curl -s http://localhost/profile) -echo "> $CURRENT_PROFILE" - -# 쉬고 있는 set 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음 -if [ $CURRENT_PROFILE == set1 ] -then - IDLE_PROFILE=set2 - IDLE_PORT=8082 -elif [ $CURRENT_PROFILE == set2 ] -then - IDLE_PROFILE=set1 - IDLE_PORT=8081 -else - echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE" - echo "> set1을 할당합니다. IDLE_PROFILE: set1" - IDLE_PROFILE=set1 - IDLE_PORT=8081 -fi - -echo "> application.jar 교체" -IDLE_APPLICATION=$IDLE_PROFILE-ASAP_Server.jar -IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION - -ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH - -echo "> $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인" -IDLE_PID=$(pgrep -f $IDLE_APPLICATION) - -if [ -z $IDLE_PID ] -then - echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." -else - echo "> kill -15 $IDLE_PID" - kill -15 $IDLE_PID - sleep 20 -fi - -echo "> $IDLE_PROFILE 배포" -nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH >> /home/ubuntu/app/nohup.out 2>&1 & - -echo "> $IDLE_PROFILE 10초 후 Health check 시작" -echo "> curl -s http://localhost:$IDLE_PORT/health " -sleep 10 - -for retry_count in {1..10} -do - response=$(curl -s http://localhost:$IDLE_PORT/actuator/health) - up_count=$(echo $response | grep 'UP' | wc -l) - - if [ $up_count -ge 1 ] - then # $up_count >= 1 ("UP" 문자열이 있는지 검증) - echo "> Health check 성공" - break - else - echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다." - echo "> Health check: ${response}" - fi - - if [ $retry_count -eq 10 ] - then - echo "> Health check 실패. " - echo "> Nginx에 연결하지 않고 배포를 종료합니다." - exit 1 - fi - - echo "> Health check 연결 실패. 재시도..." - sleep 10 -done - -echo "> 스위칭" -sleep 10 -/home/ubuntu/app/nonstop/switch.sh diff --git a/src/main/java/com/asap/server/common/exception/Error.java b/src/main/java/com/asap/server/common/exception/Error.java index ab0402ee..ee86361e 100644 --- a/src/main/java/com/asap/server/common/exception/Error.java +++ b/src/main/java/com/asap/server/common/exception/Error.java @@ -14,13 +14,18 @@ public enum Error { **/ INVALID_MEETING_URL_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않는 URL 입니다."), VALIDATION_REQUEST_MISSING_EXCEPTION(HttpStatus.BAD_REQUEST, "요청값이 유효하지 않습니다."), + ILLEGAL_ARGUMENT_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 값이 입력되었습니다."), DUPLICATED_DATE_EXCEPTION(HttpStatus.BAD_REQUEST, "중복 입력된 날짜가 있습니다."), DUPLICATED_TIME_EXCEPTION(HttpStatus.BAD_REQUEST, "중복 입력된 시간이 있습니다."), INVALID_TIME_RANGE(HttpStatus.BAD_REQUEST, "입력한 시간이 회의 가능 일시에 해당하지 않습니다."), INVALID_JSON_INPUT_EXCEPTION(HttpStatus.BAD_REQUEST, "입력 형식이 맞지 않습니다."), INVALID_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 토큰을 입력했습니다."), BAD_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청이 있습니다."), - INVALID_DATE_FORMAT_EXCEPTION(HttpStatus.BAD_REQUEST, "유요하지 않은 날짜를 입력했습니다."), + INVALID_DATE_FORMAT_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 날짜를 입력했습니다."), + // user + USERNAME_NOT_NULL_EXCEPTION(HttpStatus.BAD_REQUEST, "사용자 이름에는 null이 들어올 수 없습니다."), + USERNAME_NOT_BLANK_EXCEPTION(HttpStatus.BAD_REQUEST, "사용자 이름에는 빈 값이 들어올 수 없습니다."), + USERNAME_TOO_LONG_EXCEPTION(HttpStatus.BAD_REQUEST, "사용자 이름의 최대 입력 길이(8자)를 초과했습니다."), /** * 401 UNAUTHORIZED **/ @@ -53,7 +58,7 @@ public enum Error { * 409 CONFLICT */ MEETING_VALIDATION_FAILED_EXCEPTION(HttpStatus.CONFLICT, "이미 확정된 회의입니다."), - HOST_TIME_EXIST_EXCEPTION(HttpStatus.CONFLICT, "이미 방장의 회의 가능시간이 이미 존재합니다."), + HOST_TIME_EXIST_EXCEPTION(HttpStatus.CONFLICT, "이미 가능 시간 입력을 마쳤습니다."), /* * 429 TOO MANY REQUEST */ diff --git a/src/main/java/com/asap/server/common/exception/Success.java b/src/main/java/com/asap/server/common/exception/Success.java index 35b0de75..68c99f16 100644 --- a/src/main/java/com/asap/server/common/exception/Success.java +++ b/src/main/java/com/asap/server/common/exception/Success.java @@ -19,6 +19,7 @@ public enum Success { MEETING_VALIDATION_SUCCESS(HttpStatus.OK, "유효한 회의입니다."), LOGIN_SUCCESS(HttpStatus.OK, "로그인 성공입니다"), BEST_MEETING_SUCCESS(HttpStatus.OK, "최적의 회의시간 조회 성공입니다."), + GET_METRICS_SUCCESS(HttpStatus.OK, "메트릭 정보 조회 성공입니다."), /** * 201 CREATED SUCCESS */ diff --git a/src/main/java/com/asap/server/common/exception/model/HostTimeForbiddenException.java b/src/main/java/com/asap/server/common/exception/model/HostTimeForbiddenException.java index bbcf530d..662fce90 100644 --- a/src/main/java/com/asap/server/common/exception/model/HostTimeForbiddenException.java +++ b/src/main/java/com/asap/server/common/exception/model/HostTimeForbiddenException.java @@ -1,14 +1,13 @@ package com.asap.server.common.exception.model; -import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; import com.asap.server.common.exception.Error; import lombok.Getter; @Getter public class HostTimeForbiddenException extends AsapException { - private final HostLoginResponseDto data; + private final String data; - public HostTimeForbiddenException(Error error, HostLoginResponseDto data) { + public HostTimeForbiddenException(final Error error, final String data) { super(error); this.data = data; } diff --git a/src/main/java/com/asap/server/infra/slack/MetricsEvent.java b/src/main/java/com/asap/server/infra/slack/MetricsEvent.java new file mode 100644 index 00000000..5a9ad1e3 --- /dev/null +++ b/src/main/java/com/asap/server/infra/slack/MetricsEvent.java @@ -0,0 +1,6 @@ +package com.asap.server.infra.slack; + +import java.util.Map; + +public record MetricsEvent(Map metrics) { +} diff --git a/src/main/java/com/asap/server/infra/slack/MetricsSlackEventListener.java b/src/main/java/com/asap/server/infra/slack/MetricsSlackEventListener.java new file mode 100644 index 00000000..a6ade980 --- /dev/null +++ b/src/main/java/com/asap/server/infra/slack/MetricsSlackEventListener.java @@ -0,0 +1,60 @@ +package com.asap.server.infra.slack; + +import static com.slack.api.model.block.composition.BlockCompositions.plainText; + +import com.slack.api.Slack; +import com.slack.api.model.block.Blocks; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.BlockCompositions; +import com.slack.api.webhook.WebhookPayloads; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +public class MetricsSlackEventListener { + @Value("${slack.webhook.metrics-url}") + private String webhookUrl; + + @Async + @EventListener + public void sendMetrics(final MetricsEvent metricsEvent) throws IOException { + List layoutBlocks = generateLayoutBlock(metricsEvent.metrics()); + + Slack.getInstance().send(webhookUrl, WebhookPayloads + .payload(p -> p.blocks(layoutBlocks))); + } + + private List generateLayoutBlock(final Map metrics) { + return Blocks.asBlocks( + getHeader("ASAP 데이터"), + Blocks.divider(), + getSection(generateMetricsMessage(metrics)) + ); + } + + private String generateMetricsMessage(Map metrics) { + StringBuilder sb = new StringBuilder(); + + metrics.forEach((key, value) -> { + sb.append(key).append(" : ").append(value).append("\n"); + }); + + return sb.toString(); + } + + private LayoutBlock getHeader(final String text) { + return Blocks.header(h -> h.text( + plainText(pt -> pt.emoji(true) + .text(text)))); + } + + private LayoutBlock getSection(final String message) { + return Blocks.section(s -> + s.text(BlockCompositions.markdownText(message))); + } +} diff --git a/src/main/java/com/asap/server/infra/slack/SlackUtil.java b/src/main/java/com/asap/server/infra/slack/SlackUtil.java index 8a44713c..8b14fcaf 100644 --- a/src/main/java/com/asap/server/infra/slack/SlackUtil.java +++ b/src/main/java/com/asap/server/infra/slack/SlackUtil.java @@ -4,6 +4,7 @@ import com.slack.api.model.block.Blocks; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.composition.BlockCompositions; +import com.slack.api.webhook.Payload; import com.slack.api.webhook.WebhookPayloads; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,29 +26,19 @@ public class SlackUtil { @Value("${slack.webhook.url}") private String webhookUrl; - private StringBuilder sb= new StringBuilder(); + private final StringBuilder sb= new StringBuilder(); - private String NEW_LINE = "\n"; - private String DOUBLE_NEW_LINE = "\n\n"; + private final String NEW_LINE = "\n"; + private final String DOUBLE_NEW_LINE = "\n\n"; - //slack으로 알림보내기 public void sendAlert(Exception error, HttpServletRequest request) throws IOException{ - // 메시지 내용인 LayoutBlock List 생성 - List layoutBlocks= generateLayoutBlock(error,request); + List layoutBlocks= generateLayoutBlock(error,request); - // 슬랙의 send API과 webhookURL을 통해 생성한 메시지 내용 전송 Slack.getInstance().send(webhookUrl, WebhookPayloads - .payload(p-> - // 메시지 전송 유저명 - p.username("Exception is detected") - // 메시지 전송 유저 아이콘 이미지 URL - .iconUrl("https://yt3.googleusercontent.com/ytc/AGIKgqMVUzRrhoo1gDQcqvPo0PxaJz7e0gqDXT0D78R5VQ=s900-c-k-c0x00ffffff-no-rj") - // 메시지 내용 - .blocks(layoutBlocks))); + .payload(p -> p.blocks(layoutBlocks))); } - // 전체 메시지가 담긴 LayoutBlock 생성 - private List generateLayoutBlock(Exception error, HttpServletRequest request) { + private List generateLayoutBlock(Exception error, HttpServletRequest request) { return Blocks.asBlocks( getHeader("Internal Server Error Detected"), Blocks.divider(), @@ -55,44 +46,39 @@ private List generateLayoutBlock(Exception error, HttpServletRequest request) { Blocks.divider(), getSection(generateErrorPointMessage(request)), Blocks.divider(), - // 이슈 생성을 위해 프로젝트의 Issue URL을 입력하여 바로가기 링크를 생성 getSection("") ); } - // 예외 정보 메시지 생성 private String generateErrorMessage(Exception error) { sb.setLength(0); - sb.append("*[Exception]*" + NEW_LINE + error.toString() + DOUBLE_NEW_LINE); - sb.append("*[From]*" + NEW_LINE + readRootStackTrace(error) + DOUBLE_NEW_LINE); + sb.append("*[Exception]*").append(NEW_LINE).append(error.toString()).append(DOUBLE_NEW_LINE); + sb.append("*[From]*").append(NEW_LINE).append(readRootStackTrace(error)).append(DOUBLE_NEW_LINE); return sb.toString(); } - // HttpServletRequest를 사용하여 예외발생 요청에 대한 정보 메시지 생성 private String generateErrorPointMessage(HttpServletRequest request) { sb.setLength(0); - sb.append("*[Details]*" + NEW_LINE); - sb.append("Request URL : " + request.getRequestURL().toString() + NEW_LINE); - sb.append("Request Method : " + request.getMethod() + NEW_LINE); - sb.append("Request Time : " + new Date() + NEW_LINE); + sb.append("*[Details]*").append(NEW_LINE); + sb.append("Request URL : ").append(request.getRequestURL().toString()).append(NEW_LINE); + sb.append("Request Method : ").append(request.getMethod()).append(NEW_LINE); + sb.append("Request Time : ").append(new Date()).append(NEW_LINE); return sb.toString(); } - // 예외발생 클래스 정보 return private String readRootStackTrace(Exception error) { return error.getStackTrace()[0].toString(); } - // 에러 로그 메시지의 제목 return + private LayoutBlock getHeader(String text) { return Blocks.header(h->h.text( plainText(pt->pt.emoji(true) .text(text)))); } - // 에러 로그 메시지 내용 return private LayoutBlock getSection(String message) { return Blocks.section(s-> s.text(BlockCompositions.markdownText(message))); diff --git a/src/main/java/com/asap/server/persistence/domain/Meeting.java b/src/main/java/com/asap/server/persistence/domain/Meeting.java index f87032ba..afd43dd5 100644 --- a/src/main/java/com/asap/server/persistence/domain/Meeting.java +++ b/src/main/java/com/asap/server/persistence/domain/Meeting.java @@ -1,6 +1,8 @@ package com.asap.server.persistence.domain; import com.asap.server.persistence.domain.enums.Duration; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -11,14 +13,13 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToOne; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @Getter @Builder @@ -58,8 +59,8 @@ public boolean authenticateHost(final Long userId) { return this.host.getId().equals(userId); } - public boolean checkHostName(final String name) { - return this.host.getName().equals(name); + public boolean checkHostName(final Name name) { + return this.host.getName().equals(name.getValue()); } public boolean isConfirmedMeeting() { diff --git a/src/main/java/com/asap/server/persistence/domain/TimeBlock.java b/src/main/java/com/asap/server/persistence/domain/TimeBlock.java deleted file mode 100644 index 40bae8e7..00000000 --- a/src/main/java/com/asap/server/persistence/domain/TimeBlock.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.asap.server.persistence.domain; - -import com.asap.server.persistence.domain.enums.TimeSlot; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Enumerated; -import jakarta.persistence.ManyToOne; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.ColumnDefault; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TimeBlock extends AuditingTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ColumnDefault(value = "0") - private int weight; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "available_date_id") - private AvailableDate availableDate; - - @Enumerated(value = EnumType.STRING) - private TimeSlot timeSlot; - - @OneToMany(mappedBy = "timeBlock") - private List timeBlockUsers; - - public void addTimeBlockUsers(TimeBlockUser timeBlockUser) { - if (timeBlockUsers == null) { - timeBlockUsers = new ArrayList<>(); - } - timeBlockUsers.add(timeBlockUser); - } - - public void addWeight(int weight) { - this.weight += weight; - } -} diff --git a/src/main/java/com/asap/server/persistence/domain/TimeBlockUser.java b/src/main/java/com/asap/server/persistence/domain/TimeBlockUser.java deleted file mode 100644 index 7d0047f6..00000000 --- a/src/main/java/com/asap/server/persistence/domain/TimeBlockUser.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.asap.server.persistence.domain; - -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TimeBlockUser extends AuditingTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "time_block_id") - private TimeBlock timeBlock; -} \ No newline at end of file diff --git a/src/main/java/com/asap/server/persistence/domain/time/UserMeetingSchedule.java b/src/main/java/com/asap/server/persistence/domain/time/UserMeetingSchedule.java new file mode 100644 index 00000000..eb11a460 --- /dev/null +++ b/src/main/java/com/asap/server/persistence/domain/time/UserMeetingSchedule.java @@ -0,0 +1,57 @@ +package com.asap.server.persistence.domain.time; + +import com.asap.server.persistence.domain.AuditingTimeEntity; +import com.asap.server.persistence.domain.enums.TimeSlot; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserMeetingSchedule extends AuditingTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private Long userId; + @Column(nullable = false) + private Long meetingId; + @Column(nullable = false) + private LocalDate availableDate; + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + private TimeSlot startTimeSlot; + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + private TimeSlot endTimeSlot; + @ColumnDefault(value = "0") + private int weight; + + @Builder + private UserMeetingSchedule( + final Long userId, + final Long meetingId, + final LocalDate availableDate, + final TimeSlot startTimeSlot, + final TimeSlot endTimeSlot, + final int weight + ) { + this.userId = userId; + this.meetingId = meetingId; + this.availableDate = availableDate; + this.startTimeSlot = startTimeSlot; + this.endTimeSlot = endTimeSlot; + this.weight = weight; + } +} diff --git a/src/main/java/com/asap/server/persistence/domain/user/Name.java b/src/main/java/com/asap/server/persistence/domain/user/Name.java new file mode 100644 index 00000000..3b23ca84 --- /dev/null +++ b/src/main/java/com/asap/server/persistence/domain/user/Name.java @@ -0,0 +1,36 @@ +package com.asap.server.persistence.domain.user; + +import static com.asap.server.common.exception.Error.USERNAME_NOT_BLANK_EXCEPTION; +import static com.asap.server.common.exception.Error.USERNAME_NOT_NULL_EXCEPTION; +import static com.asap.server.common.exception.Error.USERNAME_TOO_LONG_EXCEPTION; + +import com.asap.server.common.exception.model.BadRequestException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@NoArgsConstructor +@Getter +public class Name { + @Column(nullable = false, name = "name") + private String value; + + private static final int MAX_NAME_LENGTH = 8; + + public Name(final String value) { + if (value == null) { + throw new BadRequestException(USERNAME_NOT_NULL_EXCEPTION); + } + if (value.isBlank()) { + throw new BadRequestException(USERNAME_NOT_BLANK_EXCEPTION); + } + if (value.trim().length() > MAX_NAME_LENGTH) { + throw new BadRequestException(USERNAME_TOO_LONG_EXCEPTION); + } + + this.value = value.trim(); + } +} + diff --git a/src/main/java/com/asap/server/persistence/domain/User.java b/src/main/java/com/asap/server/persistence/domain/user/User.java similarity index 68% rename from src/main/java/com/asap/server/persistence/domain/User.java rename to src/main/java/com/asap/server/persistence/domain/user/User.java index 0251a1f9..a8941d1f 100644 --- a/src/main/java/com/asap/server/persistence/domain/User.java +++ b/src/main/java/com/asap/server/persistence/domain/user/User.java @@ -1,24 +1,26 @@ -package com.asap.server.persistence.domain; +package com.asap.server.persistence.domain.user; +import com.asap.server.persistence.domain.AuditingTimeEntity; +import com.asap.server.persistence.domain.Meeting; import com.asap.server.persistence.domain.enums.Role; -import jakarta.persistence.FetchType; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.ColumnDefault; - - import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; @Entity @Getter @@ -34,8 +36,8 @@ public class User extends AuditingTimeEntity { @JoinColumn(name = "meeting_id") private Meeting meeting; - @Column(nullable = false) - private String name; + @Embedded + private Name name; @ColumnDefault(value = "false") private Boolean isFixed; @@ -43,4 +45,16 @@ public class User extends AuditingTimeEntity { @Column(nullable = false) @Enumerated(value = EnumType.STRING) private Role role; + + public String getName() { + return this.name.getValue(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof User)) { + return false; + } + return Objects.equals(this.id, ((User) obj).getId()); + } } diff --git a/src/main/java/com/asap/server/persistence/repository/TimeBlockUserRepository.java b/src/main/java/com/asap/server/persistence/repository/TimeBlockUserRepository.java deleted file mode 100644 index 0d5900c8..00000000 --- a/src/main/java/com/asap/server/persistence/repository/TimeBlockUserRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.asap.server.persistence.repository; - -import com.asap.server.persistence.domain.TimeBlock; -import com.asap.server.persistence.domain.TimeBlockUser; -import com.asap.server.persistence.domain.User; -import org.springframework.data.repository.Repository; - -import java.util.List; - -public interface TimeBlockUserRepository extends Repository { - - void save(final TimeBlockUser timeBlockUser); - - List findAllByUser(final User user); - - List findByTimeBlock(final TimeBlock timeBlock); -} diff --git a/src/main/java/com/asap/server/persistence/repository/UserMeetingScheduleRepository.java b/src/main/java/com/asap/server/persistence/repository/UserMeetingScheduleRepository.java new file mode 100644 index 00000000..2095ca81 --- /dev/null +++ b/src/main/java/com/asap/server/persistence/repository/UserMeetingScheduleRepository.java @@ -0,0 +1,13 @@ +package com.asap.server.persistence.repository; + +import com.asap.server.persistence.domain.time.UserMeetingSchedule; +import java.util.List; +import org.springframework.data.repository.Repository; + +public interface UserMeetingScheduleRepository extends Repository { + void save(final UserMeetingSchedule userMeetingSchedule); + + List findAllByMeetingId(final long meetingId); + + int countAllByUserId(final long hostId); +} diff --git a/src/main/java/com/asap/server/persistence/repository/internal/MetricsRepository.java b/src/main/java/com/asap/server/persistence/repository/internal/MetricsRepository.java new file mode 100644 index 00000000..02e3feef --- /dev/null +++ b/src/main/java/com/asap/server/persistence/repository/internal/MetricsRepository.java @@ -0,0 +1,61 @@ +package com.asap.server.persistence.repository.internal; + +import static com.asap.server.persistence.domain.QMeeting.meeting; +import static com.asap.server.persistence.domain.user.QUser.user; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.DateTimePath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MetricsRepository { + private final JPAQueryFactory jpaQueryFactory; + + public Long countTotalMeetingCount(final LocalDateTime from, final LocalDateTime to) { + return jpaQueryFactory + .select(meeting.count()) + .from(meeting) + .where(generateDateFilter(meeting.createdAt, from, to)) + .fetchOne(); + } + + public Long countTotalConfirmedMeetingCount(final LocalDateTime from, final LocalDateTime to) { + return jpaQueryFactory + .select(meeting.count()) + .from(meeting) + .where( + meeting.confirmedDateTime.confirmedStartTime.isNotNull() + .and(generateDateFilter(meeting.createdAt, from, to)) + ) + .fetchOne(); + } + + public Long countTotalUserCount(final LocalDateTime from, final LocalDateTime to) { + return jpaQueryFactory + .select(user.count()) + .from(user) + .where(generateDateFilter(user.createdAt, from, to)) + .fetchOne(); + } + + private BooleanExpression generateDateFilter( + final DateTimePath createdAt, + final LocalDateTime from, + final LocalDateTime to + ) { + if (from != null && to != null) { + return createdAt.between(from, to); + } + if (from != null) { + return createdAt.after(from); + } + if (to != null) { + return createdAt.before(to); + } + return null; + } +} diff --git a/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryCustom.java b/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryCustom.java index dcbd1e63..9c4f1e47 100644 --- a/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryCustom.java +++ b/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryCustom.java @@ -5,5 +5,5 @@ import java.util.Optional; public interface MeetingRepositoryCustom { - Optional findByIdWithHost(final Long id); + Optional findByIdWithHost(final long id); } diff --git a/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryImpl.java b/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryImpl.java index 7ece5fd6..0be45515 100644 --- a/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryImpl.java +++ b/src/main/java/com/asap/server/persistence/repository/meeting/MeetingRepositoryImpl.java @@ -1,7 +1,7 @@ package com.asap.server.persistence.repository.meeting; import static com.asap.server.persistence.domain.QMeeting.meeting; -import static com.asap.server.persistence.domain.QUser.user; +import static com.asap.server.persistence.domain.user.QUser.user; import com.asap.server.persistence.domain.Meeting; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -13,7 +13,7 @@ public class MeetingRepositoryImpl implements MeetingRepositoryCustom { private final JPAQueryFactory queryFactory; @Override - public Optional findByIdWithHost(Long id) { + public Optional findByIdWithHost(final long id) { return Optional.ofNullable( queryFactory.selectFrom(meeting) .where(meeting.id.eq(id)) diff --git a/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepository.java b/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepository.java deleted file mode 100644 index 887a4673..00000000 --- a/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.asap.server.persistence.repository.timeblock; - -import com.asap.server.persistence.domain.AvailableDate; -import com.asap.server.persistence.domain.TimeBlock; -import com.asap.server.persistence.domain.enums.TimeSlot; -import org.springframework.data.repository.Repository; - -import java.util.List; -import java.util.Optional; - -public interface TimeBlockRepository extends Repository, TimeBlockRepositoryCustom { - - void save(final TimeBlock timeBlock); - - List findByAvailableDate(final AvailableDate availableDate); - - Optional findByAvailableDateAndTimeSlot(final AvailableDate availableDate, TimeSlot timeSlot); -} diff --git a/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryCustom.java b/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryCustom.java deleted file mode 100644 index 1dd2afb3..00000000 --- a/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryCustom.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.asap.server.persistence.repository.timeblock; - -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import java.util.List; - -public interface TimeBlockRepositoryCustom { - List findAllTimeBlockByMeeting(final Long meetingId); -} diff --git a/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryImpl.java b/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryImpl.java deleted file mode 100644 index 30362dce..00000000 --- a/src/main/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.asap.server.persistence.repository.timeblock; - -import static com.asap.server.persistence.domain.QAvailableDate.availableDate; -import static com.asap.server.persistence.domain.QTimeBlock.timeBlock; -import static com.asap.server.persistence.domain.QTimeBlockUser.timeBlockUser; - -import com.asap.server.persistence.repository.timeblock.dto.QTimeBlockDto; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class TimeBlockRepositoryImpl implements TimeBlockRepositoryCustom { - private final JPAQueryFactory queryFactory; - - @Override - public List findAllTimeBlockByMeeting(Long meetingId) { - return queryFactory.select( - new QTimeBlockDto( - availableDate.date.as("availableDate"), - timeBlock.timeSlot.as("timeSlot"), - timeBlock.weight.min().as("weight"), - timeBlockUser.user.id.count().as("userCount") - ) - ).from(availableDate) - .innerJoin(timeBlock).on(timeBlock.availableDate.id.eq(availableDate.id)) - .innerJoin(timeBlockUser).on(timeBlockUser.timeBlock.id.eq(timeBlock.id)) - .where(availableDate.meeting.id.eq(meetingId)) - .groupBy(availableDate.date, timeBlock.timeSlot) - .fetch(); - } -} diff --git a/src/main/java/com/asap/server/persistence/repository/timeblock/dto/TimeBlockDto.java b/src/main/java/com/asap/server/persistence/repository/timeblock/dto/TimeBlockDto.java deleted file mode 100644 index 8a739332..00000000 --- a/src/main/java/com/asap/server/persistence/repository/timeblock/dto/TimeBlockDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.asap.server.persistence.repository.timeblock.dto; - -import com.asap.server.persistence.domain.enums.TimeSlot; -import com.querydsl.core.annotations.QueryProjection; -import org.jetbrains.annotations.NotNull; - -import java.time.LocalDate; - -public record TimeBlockDto( - LocalDate availableDate, - TimeSlot timeSlot, - int weight, - Long userCount -) implements Comparable { - @QueryProjection - public TimeBlockDto { - } - - @Override - public int compareTo(@NotNull TimeBlockDto o) { - if (this.availableDate.equals(o.availableDate)) - return Integer.compare(this.timeSlot.ordinal(), o.timeSlot.ordinal()); - return this.availableDate.compareTo(o.availableDate); - } -} diff --git a/src/main/java/com/asap/server/persistence/repository/user/UserRepository.java b/src/main/java/com/asap/server/persistence/repository/user/UserRepository.java index 15dbef15..d0d8dba1 100644 --- a/src/main/java/com/asap/server/persistence/repository/user/UserRepository.java +++ b/src/main/java/com/asap/server/persistence/repository/user/UserRepository.java @@ -1,7 +1,7 @@ package com.asap.server.persistence.repository.user; import com.asap.server.persistence.domain.Meeting; -import com.asap.server.persistence.domain.User; +import com.asap.server.persistence.domain.user.User; import org.springframework.data.repository.Repository; import java.util.List; @@ -17,4 +17,6 @@ public interface UserRepository extends Repository, UserRepositoryCu List findByMeeting(final Meeting meeting); int countByMeeting(final Meeting meeting); + + List findAllByMeetingId(final long meetingId); } diff --git a/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryCustom.java b/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryCustom.java index 1a1a7a66..99e8e230 100644 --- a/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryCustom.java +++ b/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryCustom.java @@ -1,14 +1,8 @@ package com.asap.server.persistence.repository.user; import com.asap.server.persistence.domain.Meeting; -import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.service.vo.UserVo; - -import java.time.LocalDate; import java.util.List; public interface UserRepositoryCustom { void updateUserIsFixedByMeeting(final Meeting meeting, final List users); - - List findByAvailableDateAndTimeSlots(Long meetingId, LocalDate availableDate, List timeSlots); } diff --git a/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryImpl.java b/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryImpl.java index 9f301950..b7795fa3 100644 --- a/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryImpl.java +++ b/src/main/java/com/asap/server/persistence/repository/user/UserRepositoryImpl.java @@ -1,16 +1,9 @@ package com.asap.server.persistence.repository.user; -import static com.asap.server.persistence.domain.QAvailableDate.availableDate; -import static com.asap.server.persistence.domain.QTimeBlock.timeBlock; -import static com.asap.server.persistence.domain.QTimeBlockUser.timeBlockUser; -import static com.asap.server.persistence.domain.QUser.user; +import static com.asap.server.persistence.domain.user.QUser.user; import com.asap.server.persistence.domain.Meeting; -import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.service.vo.QUserVo; -import com.asap.server.service.vo.UserVo; import com.querydsl.jpa.impl.JPAQueryFactory; -import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; @@ -28,28 +21,4 @@ public void updateUserIsFixedByMeeting(Meeting meeting, List users) { ) .execute(); } - - @Override - public List findByAvailableDateAndTimeSlots( - Long meetingId, - LocalDate date, - List timeSlots - ) { - return queryFactory.select( - new QUserVo( - user.id, - user.name - ) - ).from(timeBlockUser) - .innerJoin(timeBlock).on(timeBlockUser.timeBlock.id.eq(timeBlock.id)) - .innerJoin(user).on(timeBlockUser.user.id.eq(user.id)) - .innerJoin(availableDate).on(timeBlock.availableDate.id.eq(availableDate.id)) - .where( - availableDate.date.eq(date) - .and(availableDate.meeting.id.eq(meetingId)) - .and(timeBlock.timeSlot.in(timeSlots)) - ) - .groupBy(user.id) - .fetch(); - } } diff --git a/src/main/java/com/asap/server/presentation/common/advice/ControllerExceptionAdvice.java b/src/main/java/com/asap/server/presentation/common/advice/ControllerExceptionAdvice.java index c4c7267f..c3137cfc 100644 --- a/src/main/java/com/asap/server/presentation/common/advice/ControllerExceptionAdvice.java +++ b/src/main/java/com/asap/server/presentation/common/advice/ControllerExceptionAdvice.java @@ -1,9 +1,7 @@ package com.asap.server.presentation.common.advice; -import com.asap.server.presentation.common.dto.ErrorDataResponse; -import com.asap.server.presentation.common.dto.ErrorResponse; -import com.asap.server.infra.slack.SlackUtil; -import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; +import static com.asap.server.common.exception.Error.METHOD_NOT_ALLOWED_EXCEPTION; + import com.asap.server.common.exception.Error; import com.asap.server.common.exception.model.AsapException; import com.asap.server.common.exception.model.BadRequestException; @@ -14,9 +12,15 @@ import com.asap.server.common.exception.model.NotFoundException; import com.asap.server.common.exception.model.TooManyRequestException; import com.asap.server.common.exception.model.UnauthorizedException; +import com.asap.server.infra.slack.SlackUtil; +import com.asap.server.presentation.common.dto.ErrorDataResponse; +import com.asap.server.presentation.common.dto.ErrorResponse; +import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import jakarta.validation.ValidationException; +import java.io.IOException; +import java.time.DateTimeException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -29,11 +33,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; -import java.io.IOException; -import java.time.DateTimeException; - -import static com.asap.server.common.exception.Error.METHOD_NOT_ALLOWED_EXCEPTION; - @Slf4j @RestControllerAdvice @RequiredArgsConstructor @@ -50,6 +49,12 @@ protected ErrorResponse handleValidException(final ValidationException e) { return ErrorResponse.error(Error.VALIDATION_REQUEST_MISSING_EXCEPTION); } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + protected ErrorResponse handleIllegalArgumentException(final IllegalArgumentException e) { + return ErrorResponse.error(Error.ILLEGAL_ARGUMENT_EXCEPTION); + } + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(DateTimeException.class) protected ErrorResponse handleDateTimeException(final DateTimeException e) { @@ -104,7 +109,8 @@ protected ErrorResponse handleForbiddenException(final ForbiddenException e) { @ResponseStatus(HttpStatus.FORBIDDEN) @ExceptionHandler(HostTimeForbiddenException.class) protected ErrorDataResponse handleForbiddenException(final HostTimeForbiddenException e) { - return ErrorDataResponse.error(e.getError(), e.getMessage(), e.getData()); + HostLoginResponseDto body = new HostLoginResponseDto(e.getData()); + return ErrorDataResponse.error(e.getError(), e.getMessage(), body); } /** diff --git a/src/main/java/com/asap/server/presentation/config/async/AsyncConfig.java b/src/main/java/com/asap/server/presentation/config/async/AsyncConfig.java new file mode 100644 index 00000000..63ae4c1d --- /dev/null +++ b/src/main/java/com/asap/server/presentation/config/async/AsyncConfig.java @@ -0,0 +1,20 @@ +package com.asap.server.presentation.config.async; + +import java.util.concurrent.Executor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@EnableAsync +public class AsyncConfig { + @Bean + public Executor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(1); + taskExecutor.setQueueCapacity(10); + taskExecutor.setMaxPoolSize(3); + return taskExecutor; + } +} diff --git a/src/main/java/com/asap/server/presentation/controller/ServerProfileController.java b/src/main/java/com/asap/server/presentation/controller/ServerProfileController.java deleted file mode 100644 index f831ab8f..00000000 --- a/src/main/java/com/asap/server/presentation/controller/ServerProfileController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.asap.server.presentation.controller; - -import io.swagger.v3.oas.annotations.Hidden; - -import java.util.Arrays; - -import lombok.RequiredArgsConstructor; -import org.springframework.core.env.Environment; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@Hidden -public class ServerProfileController { - - private final Environment env; - - @GetMapping("/profile") - public String getProfile() { - return Arrays.stream(env.getActiveProfiles()) - .findFirst() - .orElse(""); - } -} diff --git a/src/main/java/com/asap/server/presentation/controller/dto/request/AvailableTimeRequestDto.java b/src/main/java/com/asap/server/presentation/controller/dto/request/AvailableTimeRequestDto.java index 43e5c92d..03888226 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/request/AvailableTimeRequestDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/request/AvailableTimeRequestDto.java @@ -1,23 +1,22 @@ package com.asap.server.presentation.controller.dto.request; +import com.asap.server.service.time.dto.register.UserTimeRegisterDto; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; - import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; + import java.util.List; -@Getter -@NoArgsConstructor @Schema(description = "사용자 가능 시간 DTO") -public class AvailableTimeRequestDto { - @NotBlank - @Size(max = 8 , message = "이름의 최대 입력 길이(8자)를 초과했습니다.") - @Schema(description = "사용자 이름",example = "김아삽") - private String name; - - @Schema(description = "가능 일자") - private List<@Valid UserMeetingTimeSaveRequestDto> availableTimes; +public record AvailableTimeRequestDto( + @Schema(description = "사용자 이름", example = "김아삽") + String name, + @Schema(description = "가능 일자") + List<@Valid UserMeetingTimeSaveRequestDto> availableTimes +) { + public UserTimeRegisterDto toRegisterDto() { + return new UserTimeRegisterDto( + this.name(), + this.availableTimes().stream().map(UserMeetingTimeSaveRequestDto::toRegisterDto).toList() + ); + } } diff --git a/src/main/java/com/asap/server/presentation/controller/dto/request/HostLoginRequestDto.java b/src/main/java/com/asap/server/presentation/controller/dto/request/HostLoginRequestDto.java index 7549b713..6e806f1c 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/request/HostLoginRequestDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/request/HostLoginRequestDto.java @@ -1,24 +1,16 @@ package com.asap.server.presentation.controller.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; - import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -@Getter -@NoArgsConstructor @Schema(description = "방장 로그인 DTO") -public class HostLoginRequestDto { - @NotBlank - @Size(max = 8 , message = "방장 이름의 최대 입력 길이(8자)를 초과했습니다.") - @Schema(description = "방장 이름", example = "김아삽") - private String name; - - @NotBlank - @Pattern(regexp = "\\d{4,}", message = "비밀번호는 4자리 이상 숫자입니다.") - @Schema(description = "회의 비밀번호", example = "0808") - private String password; +public record HostLoginRequestDto( + @Schema(description = "방장 이름", example = "김아삽") + String name, + @NotBlank + @Pattern(regexp = "\\d{4,}", message = "비밀번호는 4자리 이상 숫자입니다.") + @Schema(description = "회의 비밀번호", example = "0808") + String password +) { } diff --git a/src/main/java/com/asap/server/presentation/controller/dto/request/MeetingSaveRequestDto.java b/src/main/java/com/asap/server/presentation/controller/dto/request/MeetingSaveRequestDto.java index 908c8a81..6a6b7f1c 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/request/MeetingSaveRequestDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/request/MeetingSaveRequestDto.java @@ -7,7 +7,6 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; - import java.util.List; @Schema(description = "회의 생성 DTO") @@ -27,8 +26,6 @@ public record MeetingSaveRequestDto( @NotNull(message = "회의 진행 시간이 입력되지 않았습니다.") Duration duration, @Schema(description = "회의 방장 이름", example = "김아삽") - @NotBlank(message = "방장의 이름이 입력되지 않았습니다.") - @Size(max = 8, message = "방장 이름의 최대 입력 길이(8자)를 초과했습니다.") String name, @Schema(description = "회의 비밀번호", example = "0808") @NotBlank(message = "회의 비밀번호가 입력되지 않았습니다.") diff --git a/src/main/java/com/asap/server/presentation/controller/dto/request/UserMeetingTimeSaveRequestDto.java b/src/main/java/com/asap/server/presentation/controller/dto/request/UserMeetingTimeSaveRequestDto.java index dcda1704..f107c5d0 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/request/UserMeetingTimeSaveRequestDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/request/UserMeetingTimeSaveRequestDto.java @@ -1,6 +1,7 @@ package com.asap.server.presentation.controller.dto.request; import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.service.time.dto.register.UserMeetingScheduleRegisterDto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -25,4 +26,13 @@ public record UserMeetingTimeSaveRequestDto( @Schema(description = "회의 가능 시간 우선 순위", example = "1") int priority ) { + public UserMeetingScheduleRegisterDto toRegisterDto() { + return new UserMeetingScheduleRegisterDto( + this.month(), + this.day(), + this.startTime(), + this.endTime(), + this.priority() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/asap/server/presentation/controller/dto/response/AvailableDatesDto.java b/src/main/java/com/asap/server/presentation/controller/dto/response/AvailableDatesDto.java index e19efb9d..84a8fb27 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/response/AvailableDatesDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/response/AvailableDatesDto.java @@ -1,19 +1,21 @@ package com.asap.server.presentation.controller.dto.response; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - +import com.asap.server.service.time.dto.retrieve.AvailableDatesRetrieveDto; import java.util.List; -@Getter -@ToString -@AllArgsConstructor -@Builder -public class AvailableDatesDto { - private String month; - private String day; - private String dayOfWeek; - private List timeSlots; +public record AvailableDatesDto( + String month, + String day, + String dayOfWeek, + List timeSlots +) { + public static List of(final List retrieveDtos) { + return retrieveDtos.stream().map( + dto -> new AvailableDatesDto( + dto.month(), + dto.day(), + dto.dayOfWeek(), + TimeSlotDto.of(dto.timeSlots()) + )).toList(); + } } diff --git a/src/main/java/com/asap/server/presentation/controller/dto/response/BestMeetingTimeResponseDto.java b/src/main/java/com/asap/server/presentation/controller/dto/response/BestMeetingTimeResponseDto.java index a80e6d48..2ff8b25b 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/response/BestMeetingTimeResponseDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/response/BestMeetingTimeResponseDto.java @@ -1,33 +1,18 @@ package com.asap.server.presentation.controller.dto.response; -import com.asap.server.service.vo.BestMeetingTimeWithUsersVo; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.ToString; - -import java.util.Arrays; +import com.asap.server.service.meeting.dto.BestMeetingTimeDto; import java.util.List; -@Getter -@ToString -@AllArgsConstructor -public class BestMeetingTimeResponseDto { - private int memberCount; - private MeetingTimeResponseDto bestDateTime; - private List otherDateTimes; - - private static final int FIRST_BEST_MEETING_INDEX = 0; - private static final int SECOND_BEST_MEETING_INDEX = 1; - private static final int THIRD_BEST_MEETING_INDEX = 2; - - public static BestMeetingTimeResponseDto of(int memberCount, List bestMeetingTimes) { +public record BestMeetingTimeResponseDto( + int memberCount, + MeetingTimeResponseDto bestDateTime, + List otherDateTimes +) { + public static BestMeetingTimeResponseDto of(final BestMeetingTimeDto bestMeetingTimeDto) { return new BestMeetingTimeResponseDto( - memberCount, - MeetingTimeResponseDto.of(bestMeetingTimes.get(FIRST_BEST_MEETING_INDEX)), - Arrays.asList( - MeetingTimeResponseDto.of(bestMeetingTimes.get(SECOND_BEST_MEETING_INDEX)), - MeetingTimeResponseDto.of(bestMeetingTimes.get(THIRD_BEST_MEETING_INDEX)) - ) + bestMeetingTimeDto.memberCount(), + bestMeetingTimeDto.bestDateTime(), + bestMeetingTimeDto.otherDateTimes() ); } } diff --git a/src/main/java/com/asap/server/presentation/controller/dto/response/HostLoginResponseDto.java b/src/main/java/com/asap/server/presentation/controller/dto/response/HostLoginResponseDto.java index 39436bb6..d5444ee0 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/response/HostLoginResponseDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/response/HostLoginResponseDto.java @@ -1,14 +1,4 @@ package com.asap.server.presentation.controller.dto.response; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - -@Getter -@Builder -@ToString -@AllArgsConstructor -public class HostLoginResponseDto { - private String accessToken; +public record HostLoginResponseDto(String accessToken) { } diff --git a/src/main/java/com/asap/server/presentation/controller/dto/response/MeetingTimeResponseDto.java b/src/main/java/com/asap/server/presentation/controller/dto/response/MeetingTimeResponseDto.java index d2a7fcb4..49cfd5a1 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/response/MeetingTimeResponseDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/response/MeetingTimeResponseDto.java @@ -1,25 +1,27 @@ package com.asap.server.presentation.controller.dto.response; import com.asap.server.common.utils.DateUtil; -import com.asap.server.service.vo.BestMeetingTimeWithUsersVo; -import com.asap.server.service.vo.UserVo; +import com.asap.server.service.meeting.dto.UserDto; +import com.asap.server.service.time.vo.BestMeetingTimeWithUsers; import java.util.List; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @Getter @ToString @AllArgsConstructor +@EqualsAndHashCode public class MeetingTimeResponseDto { private String month; private String day; private String dayOfWeek; private String startTime; private String endTime; - private List users; + private List users; - public static MeetingTimeResponseDto of(BestMeetingTimeWithUsersVo bestMeetingTime) { + public static MeetingTimeResponseDto of(BestMeetingTimeWithUsers bestMeetingTime) { if (bestMeetingTime == null) return null; return new MeetingTimeResponseDto( DateUtil.getMonth(bestMeetingTime.date()), diff --git a/src/main/java/com/asap/server/presentation/controller/dto/response/TimeSlotDto.java b/src/main/java/com/asap/server/presentation/controller/dto/response/TimeSlotDto.java index 71715ee4..965042ef 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/response/TimeSlotDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/response/TimeSlotDto.java @@ -1,35 +1,16 @@ package com.asap.server.presentation.controller.dto.response; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - +import com.asap.server.service.time.dto.retrieve.TimeBlockRetrieveDto; import java.util.List; -@Getter -@ToString -@AllArgsConstructor -@Builder -public class TimeSlotDto { - private String time; - private List userNames; - private int colorLevel; - - public void setColorLevel(final int memberCount) { - double ratio = (double) userNames.size() / memberCount; - if (ratio <= 0.2) { - this.colorLevel = 1; - } else if (ratio <= 0.4) { - this.colorLevel = 2; - } else if (ratio <= 0.6) { - this.colorLevel = 3; - } else if (ratio <= 0.8) { - this.colorLevel = 4; - } else if (ratio <= 1.0) { - this.colorLevel = 5; - } else { - this.colorLevel = 0; - } +public record TimeSlotDto ( + String time, + List userNames, + int colorLevel +) { + public static List of(List retrieveDtos) { + return retrieveDtos.stream().map( + dto -> new TimeSlotDto(dto.time(), dto.userNames(), dto.colorLevel()) + ).toList(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/asap/server/presentation/controller/dto/response/TimeTableResponseDto.java b/src/main/java/com/asap/server/presentation/controller/dto/response/TimeTableResponseDto.java index 6e0bf1fc..d6c9a43f 100644 --- a/src/main/java/com/asap/server/presentation/controller/dto/response/TimeTableResponseDto.java +++ b/src/main/java/com/asap/server/presentation/controller/dto/response/TimeTableResponseDto.java @@ -1,17 +1,17 @@ package com.asap.server.presentation.controller.dto.response; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - +import com.asap.server.service.time.dto.retrieve.TimeTableRetrieveDto; import java.util.List; -@Getter -@Builder -@ToString -@AllArgsConstructor -public class TimeTableResponseDto { - public int memberCount; - public List totalUserNames; - private List availableDateTimes; + +public record TimeTableResponseDto( + int memberCount, + List totalUserNames, + List availableDateTimes +) { + public static TimeTableResponseDto of(final TimeTableRetrieveDto retrieveDto) { + return new TimeTableResponseDto( + retrieveDto.memberCount(), + retrieveDto.totalUserNames(), + AvailableDatesDto.of(retrieveDto.availableDateTimes())); + } } diff --git a/src/main/java/com/asap/server/presentation/controller/internal/MetricsController.java b/src/main/java/com/asap/server/presentation/controller/internal/MetricsController.java new file mode 100644 index 00000000..bd13f396 --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/internal/MetricsController.java @@ -0,0 +1,27 @@ +package com.asap.server.presentation.controller.internal; + +import com.asap.server.common.exception.Success; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.controller.internal.docs.MetricsControllerDocs; +import com.asap.server.service.internal.MetricsService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/internal") +@RestController +@RequiredArgsConstructor +public class MetricsController implements MetricsControllerDocs { + private final MetricsService metricsService; + + @GetMapping("/metrics") + public SuccessResponse sendMetrics( + @RequestParam(value = "from", required = false) final String from, + @RequestParam(value = "to", required = false) final String to + ) { + metricsService.sendMetrics(from, to); + return SuccessResponse.success(Success.GET_METRICS_SUCCESS); + } +} diff --git a/src/main/java/com/asap/server/presentation/controller/internal/docs/MetricsControllerDocs.java b/src/main/java/com/asap/server/presentation/controller/internal/docs/MetricsControllerDocs.java new file mode 100644 index 00000000..562a94e4 --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/internal/docs/MetricsControllerDocs.java @@ -0,0 +1,29 @@ +package com.asap.server.presentation.controller.internal.docs; + +import com.asap.server.presentation.common.dto.ErrorResponse; +import com.asap.server.presentation.common.dto.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "ASAP 내부 API", description = "ASAP 내부에서 사용하는 API입니다.") +public interface MetricsControllerDocs { + @Operation(summary = "[메트릭 조회] ASAP에 등록된 메트릭 정보 조회 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "메트릭 정보 조회 성공입니다."), + @ApiResponse( + responseCode = "400", + description = "유효하지 않은 날짜를 입력했습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + SuccessResponse sendMetrics( + @Parameter(example = "2024-08-12") final String from, + @Parameter(example = "2024-08-15") final String to + ); +} diff --git a/src/main/java/com/asap/server/presentation/controller/meeting/MeetingRegisterController.java b/src/main/java/com/asap/server/presentation/controller/meeting/MeetingRegisterController.java new file mode 100644 index 00000000..a6655924 --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/meeting/MeetingRegisterController.java @@ -0,0 +1,43 @@ +package com.asap.server.presentation.controller.meeting; + +import com.asap.server.common.exception.Success; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.config.resolver.meeting.MeetingPathVariable; +import com.asap.server.presentation.config.resolver.user.UserId; +import com.asap.server.presentation.controller.dto.request.MeetingConfirmRequestDto; +import com.asap.server.presentation.controller.dto.request.MeetingSaveRequestDto; +import com.asap.server.presentation.controller.dto.response.MeetingSaveResponseDto; +import com.asap.server.presentation.controller.meeting.docs.MeetingRegisterControllerDocs; +import com.asap.server.service.MeetingService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +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("/meeting") +@RequiredArgsConstructor +public class MeetingRegisterController implements MeetingRegisterControllerDocs { + private final MeetingService meetingService; + + @PostMapping + @Override + public SuccessResponse create( + @RequestBody @Valid final MeetingSaveRequestDto meetingSaveRequestDto + ) { + return SuccessResponse.success(Success.CREATE_MEETING_SUCCESS, meetingService.create(meetingSaveRequestDto)); + } + + @PostMapping("/{meetingId}/confirm") + @Override + public SuccessResponse confirmMeeting( + @MeetingPathVariable final Long meetingId, + @RequestBody @Valid final MeetingConfirmRequestDto meetingConfirmRequestDto, + @UserId final Long userId + ) { + meetingService.confirmMeeting(meetingConfirmRequestDto, meetingId, userId); + return SuccessResponse.success(Success.CONFIRM_MEETING_SUCCESS); + } +} diff --git a/src/main/java/com/asap/server/presentation/controller/meeting/MeetingRetrieveController.java b/src/main/java/com/asap/server/presentation/controller/meeting/MeetingRetrieveController.java new file mode 100644 index 00000000..4ddfe726 --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/meeting/MeetingRetrieveController.java @@ -0,0 +1,80 @@ +package com.asap.server.presentation.controller.meeting; + +import com.asap.server.common.exception.Success; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.config.resolver.meeting.MeetingPathVariable; +import com.asap.server.presentation.config.resolver.user.UserId; +import com.asap.server.presentation.controller.dto.response.BestMeetingTimeResponseDto; +import com.asap.server.presentation.controller.dto.response.FixedMeetingResponseDto; +import com.asap.server.presentation.controller.dto.response.MeetingScheduleResponseDto; +import com.asap.server.presentation.controller.dto.response.MeetingTitleResponseDto; +import com.asap.server.presentation.controller.dto.response.TimeTableResponseDto; +import com.asap.server.presentation.controller.meeting.docs.MeetingRetrieveControllerDocs; +import com.asap.server.service.MeetingService; +import com.asap.server.service.meeting.MeetingRetrieveService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/meeting") +@RequiredArgsConstructor +public class MeetingRetrieveController implements MeetingRetrieveControllerDocs { + private final MeetingService meetingService; + private final MeetingRetrieveService meetingRetrieveService; + + @GetMapping("/{meetingId}/schedule") + @Override + public SuccessResponse getMeetingSchedule( + @MeetingPathVariable final Long meetingId + ) { + return SuccessResponse.success( + Success.FIND_MEETING_SCHEDULE_SUCCESS, + meetingService.getMeetingSchedule(meetingId) + ); + } + + @GetMapping("/{meetingId}/card") + @Override + public SuccessResponse getFixedMeetingInformation( + @MeetingPathVariable final Long meetingId + ) { + return SuccessResponse.success( + Success.FIXED_MEETING_SUCCESS, + meetingService.getFixedMeetingInformation(meetingId) + ); + } + + @GetMapping("/{meetingId}/timetable") + @Override + public SuccessResponse getTimeTable( + @MeetingPathVariable final Long meetingId, + @UserId final Long userId + ) { + return SuccessResponse.success( + Success.FIND_TIME_TABLE_SUCCESS, + TimeTableResponseDto.of(meetingRetrieveService.getTimeTable(userId, meetingId)) + ); + } + + @GetMapping("/{meetingId}") + @Override + public SuccessResponse getIsFixedMeeting( + @MeetingPathVariable final Long meetingId + ) { + return SuccessResponse.success(Success.MEETING_VALIDATION_SUCCESS, meetingService.getIsFixedMeeting(meetingId)); + } + + @GetMapping("/{meetingId}/details") + @Override + public SuccessResponse getBestMeetingTime( + @MeetingPathVariable final Long meetingId, + @UserId Long userId + ) { + return SuccessResponse.success( + Success.BEST_MEETING_SUCCESS, + BestMeetingTimeResponseDto.of(meetingRetrieveService.getBestMeetingTime(meetingId, userId)) + ); + } +} diff --git a/src/main/java/com/asap/server/presentation/controller/meeting/docs/MeetingRegisterControllerDocs.java b/src/main/java/com/asap/server/presentation/controller/meeting/docs/MeetingRegisterControllerDocs.java new file mode 100644 index 00000000..f302506e --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/meeting/docs/MeetingRegisterControllerDocs.java @@ -0,0 +1,63 @@ +package com.asap.server.presentation.controller.meeting.docs; + +import com.asap.server.presentation.common.dto.ErrorResponse; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.controller.dto.request.MeetingConfirmRequestDto; +import com.asap.server.presentation.controller.dto.request.MeetingSaveRequestDto; +import com.asap.server.presentation.controller.dto.response.MeetingSaveResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "회의", description = "회의 관련 API 입니다.") +public interface MeetingRegisterControllerDocs { + @Operation(summary = "[회의 생성 뷰] 회의 생성 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "회의가 성공적으로 생성되었습니다."), + @ApiResponse(responseCode = "400", + description = "1. 요청값이 유효하지 않습니다.\n" + + "2. 입력 형식이 맞지 않습니다.\n" + + "3. 제목의 최대 입력 길이(15자)를 초과했습니다.\n" + + "4. 회의 가능 날짜 형식은 YYYY/mm/dd/ddd 입니다.\n" + + "5. 회의 형식이 입력되지 않았습니다.\n" + + "6. 회의 진행 시간이 입력되지 않았습니다.\n" + + "7. 방장의 이름이 입력되지 않았습니다." + + "8. 방장 이름의 최대 입력 길이(8자)를 초과했습니다.\n" + + "9. 회의 비밀번호가 입력되지 않았습니다.\n" + + "10. 비밀번호의 최소 입력 길이는 4자입니다.\n" + + "11. 비밀번호는 4자리 이상 숫자입니다.\n" + + "12. 추가 내용의 최대 입력 길이(50자)를 초과했습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + SuccessResponse create(final MeetingSaveRequestDto meetingSaveRequestDto); + + @Operation(summary = "[큐 카드] 큐 카드 확정 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "회의 시간 확정 성공입니다."), + @ApiResponse(responseCode = "400", + description = "1. 회의 진행 월이 입력되지 않았습니다.\n" + + "2. 회의 진행 날짜가 입력되지 않았습니다.\n" + + "3. 회의 진행 요일이 입력되지 않았습니다.\n" + + "4. 회의 시작 시간이 입력되지 않았습니다.\n" + + "5. 회의 종료 시간이 입력되지 않았습니다.\n" + + "6. 회의 진행 시간이 입력되지 않았습니다.\n" + + "7. 해당 유저는 해당 방의 방장이 아닙니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "해당 유저는 해당 방의 방장이 아닙니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "해당 유저는 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @SecurityRequirement(name = "JWT Auth") + SuccessResponse confirmMeeting( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId, + final MeetingConfirmRequestDto meetingConfirmRequestDto, + @Parameter(hidden = true) final Long userId + ); +} diff --git a/src/main/java/com/asap/server/presentation/controller/MeetingController.java b/src/main/java/com/asap/server/presentation/controller/meeting/docs/MeetingRetrieveControllerDocs.java similarity index 50% rename from src/main/java/com/asap/server/presentation/controller/MeetingController.java rename to src/main/java/com/asap/server/presentation/controller/meeting/docs/MeetingRetrieveControllerDocs.java index cc01096a..cc4b58f2 100644 --- a/src/main/java/com/asap/server/presentation/controller/MeetingController.java +++ b/src/main/java/com/asap/server/presentation/controller/meeting/docs/MeetingRetrieveControllerDocs.java @@ -1,19 +1,12 @@ -package com.asap.server.presentation.controller; +package com.asap.server.presentation.controller.meeting.docs; import com.asap.server.presentation.common.dto.ErrorResponse; import com.asap.server.presentation.common.dto.SuccessResponse; -import com.asap.server.presentation.config.resolver.meeting.MeetingPathVariable; -import com.asap.server.presentation.config.resolver.user.UserId; -import com.asap.server.presentation.controller.dto.request.MeetingConfirmRequestDto; -import com.asap.server.presentation.controller.dto.request.MeetingSaveRequestDto; import com.asap.server.presentation.controller.dto.response.BestMeetingTimeResponseDto; import com.asap.server.presentation.controller.dto.response.FixedMeetingResponseDto; -import com.asap.server.presentation.controller.dto.response.MeetingSaveResponseDto; import com.asap.server.presentation.controller.dto.response.MeetingScheduleResponseDto; import com.asap.server.presentation.controller.dto.response.MeetingTitleResponseDto; import com.asap.server.presentation.controller.dto.response.TimeTableResponseDto; -import com.asap.server.common.exception.Success; -import com.asap.server.service.MeetingService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; @@ -23,75 +16,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import jakarta.validation.Valid; @Tag(name = "회의", description = "회의 관련 API 입니다.") -@RestController -@RequestMapping("/meeting") -@RequiredArgsConstructor -public class MeetingController { - private final MeetingService meetingService; - - @Operation(summary = "[회의 생성 뷰] 회의 생성 API") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "회의가 성공적으로 생성되었습니다."), - @ApiResponse(responseCode = "400", - description = "1. 요청값이 유효하지 않습니다.\n" - + "2. 입력 형식이 맞지 않습니다.\n" - + "3. 제목의 최대 입력 길이(15자)를 초과했습니다.\n" - + "4. 회의 가능 날짜 형식은 YYYY/mm/dd/ddd 입니다.\n" - + "5. 회의 형식이 입력되지 않았습니다.\n" - + "6. 회의 진행 시간이 입력되지 않았습니다.\n" - + "7. 방장의 이름이 입력되지 않았습니다." - + "8. 방장 이름의 최대 입력 길이(8자)를 초과했습니다.\n" - + "9. 회의 비밀번호가 입력되지 않았습니다.\n" - + "10. 비밀번호의 최소 입력 길이는 4자입니다.\n" - + "11. 비밀번호는 4자리 이상 숫자입니다.\n" - + "12. 추가 내용의 최대 입력 길이(50자)를 초과했습니다.", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - @PostMapping - public SuccessResponse create( - @RequestBody @Valid final MeetingSaveRequestDto meetingSaveRequestDto - ) { - return SuccessResponse.success(Success.CREATE_MEETING_SUCCESS, meetingService.create(meetingSaveRequestDto)); - } - - @Operation(summary = "[큐 카드] 큐 카드 확정 API") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "회의 시간 확정 성공입니다."), - @ApiResponse(responseCode = "400", - description = "1. 회의 진행 월이 입력되지 않았습니다.\n" - + "2. 회의 진행 날짜가 입력되지 않았습니다.\n" - + "3. 회의 진행 요일이 입력되지 않았습니다.\n" - + "4. 회의 시작 시간이 입력되지 않았습니다.\n" - + "5. 회의 종료 시간이 입력되지 않았습니다.\n" - + "6. 회의 진행 시간이 입력되지 않았습니다.\n" - + "7. 해당 유저는 해당 방의 방장이 아닙니다.", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "401", description = "해당 유저는 해당 방의 방장이 아닙니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "404", description = "해당 유저는 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - @PostMapping("/{meetingId}/confirm") - @SecurityRequirement(name = "JWT Auth") - public SuccessResponse confirmMeeting( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId, - @RequestBody @Valid final MeetingConfirmRequestDto meetingConfirmRequestDto, - @UserId @Parameter(hidden = true) final Long userId - ) { - meetingService.confirmMeeting(meetingConfirmRequestDto, meetingId, userId); - return SuccessResponse.success(Success.CONFIRM_MEETING_SUCCESS); - } - +public interface MeetingRetrieveControllerDocs { @Operation(summary = "[가능 시간 입력 뷰] 회의 선택 시간표 제공 API") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "회의 선택지가 성공적으로 조회되었습니다."), @@ -103,12 +30,9 @@ public SuccessResponse confirmMeeting( @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @GetMapping("/{meetingId}/schedule") - public SuccessResponse getMeetingSchedule( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId - ) { - return SuccessResponse.success(Success.FIND_MEETING_SCHEDULE_SUCCESS, meetingService.getMeetingSchedule(meetingId)); - } + SuccessResponse getMeetingSchedule( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId + ); @Operation(summary = "[큐 카드] 큐 카드 조회 API") @ApiResponses(value = { @@ -119,16 +43,11 @@ public SuccessResponse getMeetingSchedule( @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @GetMapping("/{meetingId}/card") - public SuccessResponse getFixedMeetingInformation( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId - ) { - return SuccessResponse.success(Success.FIXED_MEETING_SUCCESS, meetingService.getFixedMeetingInformation(meetingId)); - } + SuccessResponse getFixedMeetingInformation( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId + ); @Operation(summary = "[방장 뷰] 종합 일정 시간표 제공 API") - @GetMapping("/{meetingId}/timetable") - @SecurityRequirement(name = "JWT Auth") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "종합 일정 시간표 조회 성공입니다."), @ApiResponse(responseCode = "401", description = "해당 유저는 해당 방의 방장이 아닙니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @@ -142,12 +61,11 @@ public SuccessResponse getFixedMeetingInformation( @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public SuccessResponse getTimeTable( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId, - @UserId @Parameter(hidden = true) final Long userId - ) { - return SuccessResponse.success(Success.FIND_TIME_TABLE_SUCCESS, meetingService.getTimeTable(userId, meetingId)); - } + @SecurityRequirement(name = "JWT Auth") + SuccessResponse getTimeTable( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId, + @Parameter(hidden = true) final Long userId + ); @Operation(summary = "[회의 입장 뷰] 회의 유효성 체크 API") @ApiResponses(value = { @@ -160,12 +78,9 @@ public SuccessResponse getTimeTable( @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @GetMapping("/{meetingId}") - public SuccessResponse getIsFixedMeeting( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId - ) { - return SuccessResponse.success(Success.MEETING_VALIDATION_SUCCESS, meetingService.getIsFixedMeeting(meetingId)); - } + SuccessResponse getIsFixedMeeting( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId + ); @Operation(summary = "[회의 일정 확정 뷰] 최적의 회의 시간 확인 API") @ApiResponses(value = { @@ -178,12 +93,9 @@ public SuccessResponse getIsFixedMeeting( @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @GetMapping("/{meetingId}/details") @SecurityRequirement(name = "JWT Auth") - public SuccessResponse getBestMeetingTime( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId, - @Parameter(hidden = true) @UserId Long userId - ) { - return SuccessResponse.success(Success.BEST_MEETING_SUCCESS, meetingService.getBestMeetingTime(meetingId, userId)); - } + SuccessResponse getBestMeetingTime( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId, + @Parameter(hidden = true) Long userId + ); } diff --git a/src/main/java/com/asap/server/presentation/controller/time/TimeRegisterController.java b/src/main/java/com/asap/server/presentation/controller/time/TimeRegisterController.java new file mode 100644 index 00000000..064320f7 --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/time/TimeRegisterController.java @@ -0,0 +1,56 @@ +package com.asap.server.presentation.controller.time; + +import com.asap.server.common.exception.Success; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.config.resolver.meeting.MeetingPathVariable; +import com.asap.server.presentation.config.resolver.user.UserId; +import com.asap.server.presentation.controller.dto.request.AvailableTimeRequestDto; +import com.asap.server.presentation.controller.dto.request.UserMeetingTimeSaveRequestDto; +import com.asap.server.presentation.controller.dto.response.UserMeetingTimeResponseDto; +import com.asap.server.presentation.controller.dto.response.UserTimeResponseDto; +import com.asap.server.presentation.controller.time.docs.TimeRegisterControllerDocs; +import com.asap.server.service.UserService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class TimeRegisterController implements TimeRegisterControllerDocs { + private final UserService userService; + + @PostMapping("/host/{meetingId}/time") + @Override + public SuccessResponse createHostTime( + @MeetingPathVariable final Long meetingId, + @RequestBody final List<@Valid @NotNull UserMeetingTimeSaveRequestDto> requestDtoList, + @UserId final Long userId + ) { + return SuccessResponse.success( + Success.CREATE_HOST_TIME_SUCCESS, + userService.createHostTime( + meetingId, + userId, + requestDtoList.stream().map(UserMeetingTimeSaveRequestDto::toRegisterDto).toList() + )); + } + + @PostMapping("/{meetingId}/time") + @Override + public SuccessResponse createMemberTime( + @MeetingPathVariable final Long meetingId, + @RequestBody @Valid final AvailableTimeRequestDto requestDto + ) { + return SuccessResponse.success( + Success.CREATE_MEETING_TIME_SUCCESS, + userService.createUserTime(meetingId, requestDto.toRegisterDto()) + ); + } +} diff --git a/src/main/java/com/asap/server/presentation/controller/UserController.java b/src/main/java/com/asap/server/presentation/controller/time/docs/TimeRegisterControllerDocs.java similarity index 50% rename from src/main/java/com/asap/server/presentation/controller/UserController.java rename to src/main/java/com/asap/server/presentation/controller/time/docs/TimeRegisterControllerDocs.java index 1e5a5337..89420289 100644 --- a/src/main/java/com/asap/server/presentation/controller/UserController.java +++ b/src/main/java/com/asap/server/presentation/controller/time/docs/TimeRegisterControllerDocs.java @@ -1,17 +1,11 @@ -package com.asap.server.presentation.controller; +package com.asap.server.presentation.controller.time.docs; import com.asap.server.presentation.common.dto.ErrorResponse; import com.asap.server.presentation.common.dto.SuccessResponse; -import com.asap.server.presentation.config.resolver.meeting.MeetingPathVariable; -import com.asap.server.presentation.config.resolver.user.UserId; import com.asap.server.presentation.controller.dto.request.AvailableTimeRequestDto; -import com.asap.server.presentation.controller.dto.request.HostLoginRequestDto; import com.asap.server.presentation.controller.dto.request.UserMeetingTimeSaveRequestDto; -import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; import com.asap.server.presentation.controller.dto.response.UserMeetingTimeResponseDto; import com.asap.server.presentation.controller.dto.response.UserTimeResponseDto; -import com.asap.server.common.exception.Success; -import com.asap.server.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; @@ -21,25 +15,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; import java.util.List; -@Tag(name = "사용자", description = "사용자 관련 로그인 및 가능 시간 입력 API 입니다.") -@RestController -@Validated -@RequestMapping("/user") -@RequiredArgsConstructor -public class UserController { - private final UserService userService; - +@Tag(name = "시간 입력 API", description = "방장 또는 참여자의 회의 가능한 시간을 입력하는 API") +public interface TimeRegisterControllerDocs { @Operation(summary = "[회의 가능 시간 입력 뷰 - 방장] 방장 가능 시간 입력 API") @SecurityRequirement(name = "JWT Auth") @ApiResponses(value = { @@ -58,14 +37,11 @@ public class UserController { @ApiResponse(responseCode = "409", description = "해당 회의 방장의 가능시간이 이미 존재합니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @PostMapping("/host/{meetingId}/time") - public SuccessResponse createHostTime( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId, - @RequestBody final List<@Valid @NotNull UserMeetingTimeSaveRequestDto> requestDtoList, - @UserId @Parameter(hidden = true) final Long userId - ) { - return SuccessResponse.success(Success.CREATE_HOST_TIME_SUCCESS, userService.createHostTime(meetingId, userId, requestDtoList)); - } + SuccessResponse createHostTime( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId, + final List requestDtoList, + @Parameter(hidden = true) final Long userId + ); @Operation(summary = "[회의 가능 시간 입력 뷰 - 참여자] 참여자 정보 및 가능 시간 입력 API") @ApiResponses(value = { @@ -77,31 +53,8 @@ public SuccessResponse createHostTime( content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "404", description = "해당 회의는 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @PostMapping("/{meetingId}/time") - public SuccessResponse createMemberTime( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId, - @RequestBody @Valid final AvailableTimeRequestDto requestDto - ) { - return SuccessResponse.success(Success.CREATE_MEETING_TIME_SUCCESS, userService.createUserTime(meetingId, requestDto)); - } - - @Operation(summary = "[방장 입장 뷰] 방장 로그인 API") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "로그인 성공입니다"), - @ApiResponse(responseCode = "400", - description = "1. 방장 이름의 최대 입력 길이(8자)를 초과했습니다.\n" - + "2. 비밀번호는 4자리 이상 숫자입니다.", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "401", description = "유효하지 않은 사용자 이름 또는 비밀번호입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "403", description = "회의 가능 시간이 입력되지 않았습니다."), - @ApiResponse(responseCode = "404", description = "해당 회의는 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - @PostMapping("{meetingId}/host") - public SuccessResponse loginByHost( - @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) @MeetingPathVariable final Long meetingId, - @RequestBody @Valid final HostLoginRequestDto requestDto - ) { - return SuccessResponse.success(Success.LOGIN_SUCCESS, userService.loginByHost(meetingId, requestDto)); - } + SuccessResponse createMemberTime( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId, + final AvailableTimeRequestDto requestDto + ); } diff --git a/src/main/java/com/asap/server/presentation/controller/user/UserRegisterController.java b/src/main/java/com/asap/server/presentation/controller/user/UserRegisterController.java new file mode 100644 index 00000000..36881930 --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/user/UserRegisterController.java @@ -0,0 +1,40 @@ +package com.asap.server.presentation.controller.user; + +import com.asap.server.common.exception.Success; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.config.resolver.meeting.MeetingPathVariable; +import com.asap.server.presentation.controller.dto.request.HostLoginRequestDto; +import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; +import com.asap.server.presentation.controller.user.docs.UserRegisterControllerDocs; +import com.asap.server.service.user.UserLoginService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +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("/user") +@RequiredArgsConstructor +public class UserRegisterController implements UserRegisterControllerDocs { + private final UserLoginService userLoginService; + + @PostMapping("{meetingId}/host") + @Override + public SuccessResponse loginByHost( + @MeetingPathVariable final Long meetingId, + @RequestBody @Valid final HostLoginRequestDto requestDto + ) { + String hostAccessToken = userLoginService.loginByHost( + meetingId, + requestDto.name(), + requestDto.password() + ); + + return SuccessResponse.success( + Success.LOGIN_SUCCESS, + new HostLoginResponseDto(hostAccessToken) + ); + } +} diff --git a/src/main/java/com/asap/server/presentation/controller/user/docs/UserRegisterControllerDocs.java b/src/main/java/com/asap/server/presentation/controller/user/docs/UserRegisterControllerDocs.java new file mode 100644 index 00000000..0914d65c --- /dev/null +++ b/src/main/java/com/asap/server/presentation/controller/user/docs/UserRegisterControllerDocs.java @@ -0,0 +1,34 @@ +package com.asap.server.presentation.controller.user.docs; + +import com.asap.server.presentation.common.dto.ErrorResponse; +import com.asap.server.presentation.common.dto.SuccessResponse; +import com.asap.server.presentation.controller.dto.request.HostLoginRequestDto; +import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "사용자", description = "사용자 및 로그인 관련 API 입니다.") +public interface UserRegisterControllerDocs { + @Operation(summary = "[방장 입장 뷰] 방장 로그인 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공입니다"), + @ApiResponse(responseCode = "400", + description = "1. 방장 이름의 최대 입력 길이(8자)를 초과했습니다.\n" + + "2. 비밀번호는 4자리 이상 숫자입니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 사용자 이름 또는 비밀번호입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "회의 가능 시간이 입력되지 않았습니다."), + @ApiResponse(responseCode = "404", description = "해당 회의는 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "409", description = "이미 확정된 회의입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + SuccessResponse loginByHost( + @Parameter(schema = @Schema(implementation = String.class), in = ParameterIn.PATH) final Long meetingId, + final HostLoginRequestDto requestDto + ); +} diff --git a/src/main/java/com/asap/server/service/AvailableDateService.java b/src/main/java/com/asap/server/service/AvailableDateService.java index 5f01f237..6ac7701e 100644 --- a/src/main/java/com/asap/server/service/AvailableDateService.java +++ b/src/main/java/com/asap/server/service/AvailableDateService.java @@ -1,15 +1,13 @@ package com.asap.server.service; -import com.asap.server.common.utils.DateUtil; -import com.asap.server.presentation.controller.dto.response.AvailableDateResponseDto; -import com.asap.server.presentation.controller.dto.response.AvailableDatesDto; -import com.asap.server.presentation.controller.dto.response.TimeSlotDto; -import com.asap.server.persistence.domain.AvailableDate; -import com.asap.server.persistence.domain.Meeting; import com.asap.server.common.exception.Error; import com.asap.server.common.exception.model.BadRequestException; import com.asap.server.common.exception.model.NotFoundException; +import com.asap.server.common.utils.DateUtil; +import com.asap.server.persistence.domain.AvailableDate; +import com.asap.server.persistence.domain.Meeting; import com.asap.server.persistence.repository.AvailableDateRepository; +import com.asap.server.presentation.controller.dto.response.AvailableDateResponseDto; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -23,8 +21,6 @@ public class AvailableDateService { private static final int DAY_ELEMENT_INDEX = 2; private final AvailableDateRepository availableDateRepository; - private final TimeBlockService timeBlockService; - private final TimeBlockUserService timeBlockUserService; public List getAvailableDates(final Meeting meeting) { @@ -48,29 +44,6 @@ public List findAvailableDateByMeeting(final Meeting meeting) { return availableDates; } - public AvailableDatesDto getAvailableDatesDto(final AvailableDate availableDate, final int memberCount) { - List timeSlotDtos = timeBlockService.findByAvailableDate(availableDate).stream().map( - timeBlock -> timeBlockUserService.getTimeSlotDto(timeBlock, memberCount) - ).collect(Collectors.toList()); - - return AvailableDatesDto.builder() - .timeSlots(timeSlotDtos) - .month(DateUtil.getMonth(availableDate.getDate())) - .day(DateUtil.getDay(availableDate.getDate())) - .dayOfWeek(DateUtil.getDayOfWeek(availableDate.getDate())) - .build(); - } - - ; - - public AvailableDate findByMeetingAndDate(final Meeting meeting, - final String month, - final String day) { - return availableDateRepository.findByMeetingAndDate(meeting, - DateUtil.transformLocalDate(month, day)) - .orElseThrow(() -> new NotFoundException(Error.AVAILABLE_DATE_NOT_FOUND_EXCEPTION)); - } - private List findAvailableDates(final Meeting meeting) { List availableDates = availableDateRepository.findByMeeting(meeting); diff --git a/src/main/java/com/asap/server/service/MeetingService.java b/src/main/java/com/asap/server/service/MeetingService.java index c56af2a9..3d3e8f5e 100644 --- a/src/main/java/com/asap/server/service/MeetingService.java +++ b/src/main/java/com/asap/server/service/MeetingService.java @@ -13,23 +13,18 @@ import com.asap.server.persistence.domain.ConfirmedDateTime; import com.asap.server.persistence.domain.Meeting; import com.asap.server.persistence.domain.Place; -import com.asap.server.persistence.domain.User; import com.asap.server.persistence.domain.enums.Role; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; import com.asap.server.persistence.repository.meeting.MeetingRepository; -import com.asap.server.persistence.repository.timeblock.TimeBlockRepository; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; import com.asap.server.presentation.controller.dto.request.MeetingConfirmRequestDto; import com.asap.server.presentation.controller.dto.request.MeetingSaveRequestDto; import com.asap.server.presentation.controller.dto.response.AvailableDatesDto; -import com.asap.server.presentation.controller.dto.response.BestMeetingTimeResponseDto; import com.asap.server.presentation.controller.dto.response.FixedMeetingResponseDto; import com.asap.server.presentation.controller.dto.response.MeetingSaveResponseDto; import com.asap.server.presentation.controller.dto.response.MeetingScheduleResponseDto; import com.asap.server.presentation.controller.dto.response.MeetingTitleResponseDto; import com.asap.server.presentation.controller.dto.response.TimeTableResponseDto; -import com.asap.server.service.meeting.recommend.MeetingTimeRecommendService; -import com.asap.server.service.vo.BestMeetingTimeVo; -import com.asap.server.service.vo.BestMeetingTimeWithUsersVo; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -48,8 +43,6 @@ public class MeetingService { private final UserService userService; private final AvailableDateService availableDateService; private final JwtService jwtService; - private final MeetingTimeRecommendService meetingTimeRecommendService; - private final TimeBlockRepository timeBlockRepository; private final PasswordEncoder passwordEncoder; @Transactional @@ -69,7 +62,7 @@ public MeetingSaveResponseDto create(final MeetingSaveRequestDto meetingSaveRequ meetingRepository.save(meeting); - User host = userService.createUser(meeting, meetingSaveRequestDto.name(), Role.HOST); + User host = userService.createUser(meeting, new Name(meetingSaveRequestDto.name()), Role.HOST); availableDateService.create(meeting, meetingSaveRequestDto.availableDates()); @@ -150,29 +143,6 @@ public FixedMeetingResponseDto getFixedMeetingInformation(final Long meetingId) .build(); } - public TimeTableResponseDto getTimeTable(final Long userId, final Long meetingId) { - Meeting meeting = meetingRepository.findById(meetingId) - .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); - - if (!meeting.authenticateHost(userId)) - throw new UnauthorizedException(INVALID_MEETING_HOST_EXCEPTION); - - if (meeting.isConfirmedMeeting()) - throw new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION); - - List memberNames = userService.findUserNameByMeeting(meeting); - - List availableDatesDtos = availableDateService.findAvailableDateByMeeting(meeting).stream() - .map(availableDate -> availableDateService.getAvailableDatesDto(availableDate, memberNames.size())) - .collect(Collectors.toList()); - - return TimeTableResponseDto.builder() - .totalUserNames(memberNames) - .memberCount(memberNames.size()) - .availableDateTimes(availableDatesDtos) - .build(); - } - public MeetingTitleResponseDto getIsFixedMeeting(final Long meetingId) throws ConflictException { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); @@ -184,20 +154,4 @@ public MeetingTitleResponseDto getIsFixedMeeting(final Long meetingId) throws Co .title(meeting.getTitle()) .build(); } - - public BestMeetingTimeResponseDto getBestMeetingTime(final Long meetingId, final Long userId) { - Meeting meeting = meetingRepository.findById(meetingId) - .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); - - if (!meeting.authenticateHost(userId)) throw new UnauthorizedException(Error.INVALID_MEETING_HOST_EXCEPTION); - if (meeting.isConfirmedMeeting()) throw new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION); - - int userCount = userService.getMeetingUserCount(meeting); - List timeBlocks = timeBlockRepository.findAllTimeBlockByMeeting(meetingId); - - List bestMeetingTimes = meetingTimeRecommendService.getBestMeetingTime(timeBlocks, meeting.getDuration(), userCount); - List bestMeetingTimeWithUsers = userService.getBestMeetingInUsers(meetingId, bestMeetingTimes); - return BestMeetingTimeResponseDto.of(userCount, bestMeetingTimeWithUsers); - } - } diff --git a/src/main/java/com/asap/server/service/TimeBlockService.java b/src/main/java/com/asap/server/service/TimeBlockService.java deleted file mode 100644 index 39e84578..00000000 --- a/src/main/java/com/asap/server/service/TimeBlockService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.asap.server.service; - - -import com.asap.server.persistence.domain.AvailableDate; -import com.asap.server.persistence.domain.TimeBlock; -import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.common.exception.Error; -import com.asap.server.common.exception.model.NotFoundException; -import com.asap.server.persistence.repository.timeblock.TimeBlockRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class TimeBlockService { - private final TimeBlockRepository timeBlockRepository; - - @Transactional - public TimeBlock searchTimeBlock(final TimeSlot timeSlot, - final AvailableDate availableDate, - final int weight) { - TimeBlock timeBlock = timeBlockRepository.findByAvailableDateAndTimeSlot(availableDate, timeSlot) - .orElseGet(() -> create(timeSlot, availableDate)); - - timeBlock.addWeight(weight); - timeBlockRepository.save(timeBlock); - return timeBlock; - } - - public List findByAvailableDate(final AvailableDate availableDate) { - List timeBlocks = timeBlockRepository.findByAvailableDate(availableDate); - if (timeBlocks == null) { - throw new NotFoundException(Error.TIME_BLOCK_NOT_FOUND_EXCEPTION); - } - return timeBlocks; - } - - private TimeBlock create(final TimeSlot timeSlot, final AvailableDate availableDate) { - TimeBlock timeBlock = TimeBlock.builder() - .timeSlot(timeSlot) - .availableDate(availableDate) - .build(); - - timeBlockRepository.save(timeBlock); - return timeBlock; - } - - public List getTimeBlocksByAvailableDate(final AvailableDate availableDate) { - return timeBlockRepository.findByAvailableDate(availableDate); - } -} diff --git a/src/main/java/com/asap/server/service/TimeBlockUserService.java b/src/main/java/com/asap/server/service/TimeBlockUserService.java deleted file mode 100644 index cf1babae..00000000 --- a/src/main/java/com/asap/server/service/TimeBlockUserService.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.asap.server.service; - -import com.asap.server.presentation.controller.dto.response.TimeSlotDto; -import com.asap.server.persistence.domain.TimeBlock; -import com.asap.server.persistence.domain.TimeBlockUser; -import com.asap.server.persistence.domain.User; -import com.asap.server.persistence.repository.TimeBlockUserRepository; -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 TimeBlockUserService { - private final TimeBlockUserRepository timeBlockUserRepository; - - @Transactional - public TimeBlockUser create(final TimeBlock timeBlock, final User user) { - TimeBlockUser timeBlockUser = TimeBlockUser.builder() - .user(user) - .timeBlock(timeBlock) - .build(); - timeBlockUserRepository.save(timeBlockUser); - return timeBlockUser; - } - - public List findUsersByTimeBlock(final TimeBlock timeBlock) { - return timeBlockUserRepository.findByTimeBlock(timeBlock) - .stream() - .map(TimeBlockUser::getUser) - .collect(Collectors.toList()); - } - - public TimeSlotDto getTimeSlotDto(final TimeBlock timeBlock, final int memberCount) { - TimeSlotDto timeSlotDto = TimeSlotDto.builder() - .time(timeBlock.getTimeSlot().getTime()) - .userNames(findUsersByTimeBlock(timeBlock).stream().map(User::getName).collect(Collectors.toList())) - .build(); - timeSlotDto.setColorLevel(memberCount); - return timeSlotDto; - } - - public boolean isEmptyHostTimeBlock(final User user) { - List hostTimeBlocks = timeBlockUserRepository.findAllByUser(user); - return hostTimeBlocks.isEmpty(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/asap/server/service/UserService.java b/src/main/java/com/asap/server/service/UserService.java index 9c1d67fe..644a1e9e 100644 --- a/src/main/java/com/asap/server/service/UserService.java +++ b/src/main/java/com/asap/server/service/UserService.java @@ -1,61 +1,49 @@ package com.asap.server.service; -import com.asap.server.common.jwt.JwtService; -import com.asap.server.presentation.controller.dto.request.AvailableTimeRequestDto; -import com.asap.server.presentation.controller.dto.request.HostLoginRequestDto; -import com.asap.server.presentation.controller.dto.request.UserMeetingTimeSaveRequestDto; -import com.asap.server.presentation.controller.dto.request.UserRequestDto; -import com.asap.server.presentation.controller.dto.response.HostLoginResponseDto; -import com.asap.server.presentation.controller.dto.response.UserMeetingTimeResponseDto; -import com.asap.server.presentation.controller.dto.response.UserTimeResponseDto; -import com.asap.server.persistence.domain.AvailableDate; -import com.asap.server.persistence.domain.Meeting; -import com.asap.server.persistence.domain.User; -import com.asap.server.persistence.domain.enums.Role; -import com.asap.server.persistence.domain.enums.TimeSlot; +import static com.asap.server.common.exception.Error.INVALID_MEETING_HOST_EXCEPTION; + import com.asap.server.common.exception.Error; import com.asap.server.common.exception.model.BadRequestException; import com.asap.server.common.exception.model.ConflictException; -import com.asap.server.common.exception.model.HostTimeForbiddenException; import com.asap.server.common.exception.model.NotFoundException; import com.asap.server.common.exception.model.UnauthorizedException; +import com.asap.server.common.jwt.JwtService; +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.enums.Role; +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; import com.asap.server.persistence.repository.meeting.MeetingRepository; import com.asap.server.persistence.repository.user.UserRepository; -import com.asap.server.service.vo.BestMeetingTimeVo; -import com.asap.server.service.vo.BestMeetingTimeWithUsersVo; -import com.asap.server.service.vo.UserVo; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import com.asap.server.presentation.controller.dto.request.UserRequestDto; +import com.asap.server.presentation.controller.dto.response.UserMeetingTimeResponseDto; +import com.asap.server.presentation.controller.dto.response.UserTimeResponseDto; +import com.asap.server.service.time.UserMeetingScheduleService; +import com.asap.server.service.time.dto.register.UserMeetingScheduleRegisterDto; +import com.asap.server.service.time.dto.register.UserTimeRegisterDto; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import static com.asap.server.common.exception.Error.INVALID_MEETING_HOST_EXCEPTION; -import static com.asap.server.common.exception.Error.MEETING_VALIDATION_FAILED_EXCEPTION; -import static com.asap.server.common.exception.Error.USER_NOT_FOUND_EXCEPTION; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; - private final TimeBlockService timeBlockService; - private final TimeBlockUserService timeBlockUserService; - private final AvailableDateService availableDateService; private final MeetingRepository meetingRepository; private final JwtService jwtService; - private final PasswordEncoder passwordEncoder; + private final UserMeetingScheduleService userMeetingScheduleService; public User createUser(final Meeting meeting, - final String hostName, + final Name userName, final Role role) { User user = User.builder() .meeting(meeting) - .name(hostName) + .name(userName) .role(role) .isFixed(false) .build(); @@ -64,18 +52,23 @@ public User createUser(final Meeting meeting, } @Transactional - public UserMeetingTimeResponseDto createHostTime(final Long meetingId, - final Long userId, - final List requestDtos) { + public UserMeetingTimeResponseDto createHostTime( + final Long meetingId, + final Long hostId, + final List requestDtos + ) { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); - if (!meeting.authenticateHost(userId)) + + if (!meeting.authenticateHost(hostId)) { throw new UnauthorizedException(INVALID_MEETING_HOST_EXCEPTION); - if (!timeBlockUserService.isEmptyHostTimeBlock(meeting.getHost())) + } + + if (!userMeetingScheduleService.isEmptyHostTimeBlock(meeting.getHost().getId())) { throw new ConflictException(Error.HOST_TIME_EXIST_EXCEPTION); + } - isDuplicatedDate(requestDtos); - requestDtos.forEach(requestDto -> createUserTimeBlock(meeting, meeting.getHost(), requestDto)); + userMeetingScheduleService.createUserMeetingSchedule(meetingId, hostId, requestDtos); String accessToken = jwtService.issuedToken(meeting.getHost().getId().toString()); @@ -86,54 +79,22 @@ public UserMeetingTimeResponseDto createHostTime(final Long meetingId, } @Transactional - public UserTimeResponseDto createUserTime(final Long meetingId, - final AvailableTimeRequestDto requestDto) { + public UserTimeResponseDto createUserTime( + final Long meetingId, + final UserTimeRegisterDto registerDto + ) { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); - User user = createUser(meeting, requestDto.getName(), Role.MEMBER); - isDuplicatedDate(requestDto.getAvailableTimes()); - requestDto.getAvailableTimes().forEach(availableTime -> createUserTimeBlock(meeting, user, availableTime)); + User user = createUser(meeting, new Name(registerDto.name()), Role.MEMBER); + + userMeetingScheduleService.createUserMeetingSchedule(meetingId, user.getId(), registerDto.availableSchedules()); return UserTimeResponseDto.builder() .role(Role.MEMBER.getRole()) .build(); } - public List findUserNameByMeeting(final Meeting meeting) { - List users = userRepository.findByMeeting(meeting); - if (users.isEmpty()) { - throw new NotFoundException(USER_NOT_FOUND_EXCEPTION); - } - return users.stream() - .map(User::getName) - .collect(Collectors.toList()); - } - - private void createUserTimeBlock(final Meeting meeting, - final User user, - final UserMeetingTimeSaveRequestDto requestDto) { - AvailableDate availableDate = availableDateService.findByMeetingAndDate(meeting, requestDto.month(), requestDto.day()); - TimeSlot.getTimeSlots(requestDto.startTime().ordinal(), requestDto.endTime().ordinal() - 1) - .stream() - .map(timeSlot -> timeBlockService.searchTimeBlock(timeSlot, availableDate, requestDto.priority())).collect(Collectors.toList()) - .forEach(timeBlock -> timeBlock.addTimeBlockUsers(timeBlockUserService.create(timeBlock, user))); - } - - private void isDuplicatedDate(final List requestDtoList) { - Map> meetingTimeAvailable = new HashMap<>(); - for (UserMeetingTimeSaveRequestDto requestDto : requestDtoList) { - String col = String.format("%s %s", requestDto.month(), requestDto.day()); - List timeSlots = TimeSlot.getTimeSlots(requestDto.startTime().ordinal(), requestDto.endTime().ordinal() - 1); - if (meetingTimeAvailable.containsKey(col)) { - if (meetingTimeAvailable.get(col).stream().anyMatch(timeSlots::contains)) { - throw new BadRequestException(Error.DUPLICATED_TIME_EXCEPTION); - } - } else { - meetingTimeAvailable.put(col, timeSlots); - } - } - } public List getFixedUsers(final Meeting meeting) { return userRepository @@ -150,59 +111,4 @@ public void setFixedUsers(final Meeting meeting, final List user .collect(Collectors.toList()); userRepository.updateUserIsFixedByMeeting(meeting, userIds); } - - public int getMeetingUserCount(final Meeting meeting) { - return userRepository.countByMeeting(meeting); - } - - @Transactional - public HostLoginResponseDto loginByHost( - final Long meetingId, - final HostLoginRequestDto requestDto - ) { - Meeting meeting = meetingRepository.findByIdWithHost(meetingId) - .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); - - if (!meeting.checkHostName(requestDto.getName())) - throw new UnauthorizedException(Error.INVALID_HOST_ID_PASSWORD_EXCEPTION); - - if (!passwordEncoder.matches(requestDto.getPassword(), meeting.getPassword())) - throw new UnauthorizedException(Error.INVALID_HOST_ID_PASSWORD_EXCEPTION); - - if (meeting.isConfirmedMeeting()) - throw new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION); - - HostLoginResponseDto responseDto = HostLoginResponseDto - .builder() - .accessToken(jwtService.issuedToken(meeting.getHost().getId().toString())) - .build(); - if (timeBlockUserService.isEmptyHostTimeBlock(meeting.getHost())) { - throw new HostTimeForbiddenException(Error.HOST_MEETING_TIME_NOT_PROVIDED, responseDto); - } - - return responseDto; - } - - public List getBestMeetingInUsers( - final Long meetingId, - final List bestMeetingTimes - ) { - return bestMeetingTimes.stream() - .map(bestMeetingTime -> getBestMeetingTimeInUsers(meetingId, bestMeetingTime)) - .collect(Collectors.toList()); - } - - private BestMeetingTimeWithUsersVo getBestMeetingTimeInUsers( - final Long meetingId, - final BestMeetingTimeVo bestMeetingTime - ) { - if (bestMeetingTime == null) { - return null; - } - List timeSlots = TimeSlot.getTimeSlots(bestMeetingTime.startTime().ordinal(), - bestMeetingTime.endTime().ordinal() - 1); - List users = userRepository.findByAvailableDateAndTimeSlots(meetingId, bestMeetingTime.date(), - timeSlots); - return BestMeetingTimeWithUsersVo.of(bestMeetingTime, users); - } } diff --git a/src/main/java/com/asap/server/service/internal/MetricsService.java b/src/main/java/com/asap/server/service/internal/MetricsService.java new file mode 100644 index 00000000..98b1c0a9 --- /dev/null +++ b/src/main/java/com/asap/server/service/internal/MetricsService.java @@ -0,0 +1,58 @@ +package com.asap.server.service.internal; + +import static com.asap.server.common.exception.Error.INVALID_DATE_FORMAT_EXCEPTION; + +import com.asap.server.common.exception.model.BadRequestException; +import com.asap.server.infra.slack.MetricsEvent; +import com.asap.server.persistence.repository.internal.MetricsRepository; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MetricsService { + private final MetricsRepository metricsRepository; + private final ApplicationEventPublisher publisher; + + public void sendMetrics(final String fromStr, final String toStr) { + if (!isValidDate(fromStr) || !isValidDate(toStr)) { + throw new BadRequestException(INVALID_DATE_FORMAT_EXCEPTION); + } + + LocalDateTime from = null; + LocalDateTime to = null; + if (fromStr != null) { + from = LocalDate.parse(fromStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(); + } + if (toStr != null) { + to = LocalDate.parse(toStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(); + } + + Map metrics = new HashMap<>(); + metrics.put("개설된 총 회의 수", String.valueOf(metricsRepository.countTotalMeetingCount(from, to))); + metrics.put("사용한 총 사용자 수", String.valueOf(metricsRepository.countTotalUserCount(from, to))); + metrics.put("확정된 총 회의 수", String.valueOf(metricsRepository.countTotalConfirmedMeetingCount(from, to))); + + publisher.publishEvent(new MetricsEvent(metrics)); + } + + private boolean isValidDate(final String dateStr) { + if (dateStr == null) { + return true; + } + + try { + LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); + return true; + } catch (DateTimeParseException e) { + return false; + } + } +} diff --git a/src/main/java/com/asap/server/service/meeting/MeetingRetrieveService.java b/src/main/java/com/asap/server/service/meeting/MeetingRetrieveService.java new file mode 100644 index 00000000..ab0142bb --- /dev/null +++ b/src/main/java/com/asap/server/service/meeting/MeetingRetrieveService.java @@ -0,0 +1,160 @@ +package com.asap.server.service.meeting; + +import com.asap.server.common.exception.Error; +import com.asap.server.common.exception.model.ConflictException; +import com.asap.server.common.exception.model.NotFoundException; +import com.asap.server.common.exception.model.UnauthorizedException; +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.user.User; +import com.asap.server.persistence.repository.meeting.MeetingRepository; +import com.asap.server.service.meeting.dto.BestMeetingTimeDto; +import com.asap.server.service.meeting.dto.UserDto; +import com.asap.server.service.time.MeetingTimeRecommendService; +import com.asap.server.service.time.UserMeetingScheduleService; +import com.asap.server.service.time.dto.retrieve.AvailableDatesRetrieveDto; +import com.asap.server.service.time.dto.retrieve.TimeBlockRetrieveDto; +import com.asap.server.service.time.dto.retrieve.TimeTableRetrieveDto; +import com.asap.server.service.time.vo.BestMeetingTimeVo; +import com.asap.server.service.time.vo.BestMeetingTimeWithUsers; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.user.UserRetrieveService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.asap.server.common.exception.Error.MEETING_VALIDATION_FAILED_EXCEPTION; +import static java.util.function.Predicate.isEqual; + +@Service +@RequiredArgsConstructor +public class MeetingRetrieveService { + private final MeetingRepository meetingRepository; + private final UserRetrieveService userRetrieveService; + private final MeetingTimeRecommendService meetingTimeRecommendService; + private final UserMeetingScheduleService userMeetingScheduleService; + + public BestMeetingTimeDto getBestMeetingTime(final Long meetingId, final Long userId) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); + + if (!meeting.authenticateHost(userId)) { + throw new UnauthorizedException(Error.INVALID_MEETING_HOST_EXCEPTION); + } + if (meeting.isConfirmedMeeting()) { + throw new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION); + } + + int userCount = userRetrieveService.getMeetingUserCount(meeting); + + List timeBlocks = userMeetingScheduleService.getTimeBlocks(meetingId); + + List bestMeetingTimes = meetingTimeRecommendService.getBestMeetingTime( + timeBlocks, + meeting.getDuration(), + userCount + ); + + Map userIdToUserMap = userRetrieveService.getUserIdToUserMap(meetingId); + List bestMeetingTimeWithUsers = bestMeetingTimes.stream() + .map(bestMeetingTime -> mapToBestMeetingTimeWithUsers(bestMeetingTime, userIdToUserMap)) + .toList(); + return BestMeetingTimeDto.of(userCount, bestMeetingTimeWithUsers); + } + + private BestMeetingTimeWithUsers mapToBestMeetingTimeWithUsers( + final BestMeetingTimeVo bestMeetingTime, + final Map userIdToUserMap + ) { + if (bestMeetingTime == null) { + return null; + } + + List userDtos = bestMeetingTime.userIds().stream() + .map(userId -> { + User user = userIdToUserMap.get(userId); + return new UserDto(user.getId(), user.getName()); + }) + .toList(); + + return new BestMeetingTimeWithUsers( + bestMeetingTime.date(), + bestMeetingTime.startTime(), + bestMeetingTime.endTime(), + bestMeetingTime.weight(), + userDtos + ); + } + + @Transactional(readOnly = true) + public TimeTableRetrieveDto getTimeTable(final Long userId, final Long meetingId) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); + + if (!meeting.authenticateHost(userId)) { + throw new UnauthorizedException(Error.INVALID_MEETING_HOST_EXCEPTION); + } + if (meeting.isConfirmedMeeting()) { + throw new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION); + } + Map userIdToUserMap = userRetrieveService.getUserIdToUserMap(meetingId); + + List userNames = userIdToUserMap.keySet().stream() + .sorted() + .map(id -> userIdToUserMap.get(id).getName()) + .toList(); + + return TimeTableRetrieveDto.of(userNames, getAvailableDatesDto(meetingId, userNames.size(), userIdToUserMap)); + + } + + private List getAvailableDatesDto(final Long meetingId, final int totalUserCount, final Map userIdToUserMap) { + List timeBlockVos = userMeetingScheduleService.getTimeBlocks(meetingId); + Map> timeSlotDtoMappedByDate = getTimeTableMapFromTimeBlockVo(timeBlockVos, totalUserCount, userIdToUserMap); + return timeSlotDtoMappedByDate.keySet().stream().map( + date -> AvailableDatesRetrieveDto.of( + date, + timeSlotDtoMappedByDate.get(date) + ) + ).toList(); + } + + private Map> getTimeTableMapFromTimeBlockVo(final List timeBlockVo, final int totalUserCount, final Map userIdToUserMap) { + return timeBlockVo.stream() + .collect(Collectors.groupingBy( + TimeBlockVo::availableDate, + Collectors.mapping(t -> new TimeBlockRetrieveDto( + t.timeSlot().getTime(), + t.userIds().stream() + .filter(userIdToUserMap::containsKey) + .map(id -> userIdToUserMap.get(id).getName()) + .toList(), + setColorLevel(totalUserCount, t.userIds().size()) + ), + Collectors.toList() + ) + )); + } + + private int setColorLevel(final int memberCount, final int availableUserCount) { + double ratio = (double) availableUserCount / memberCount; + + if (ratio <= 0.2) { + return 1; + } else if (ratio <= 0.4) { + return 2; + } else if (ratio <= 0.6) { + return 3; + } else if (ratio <= 0.8) { + return 4; + } else if (ratio <= 1.0) { + return 5; + } else { + return 0; + } + } +} diff --git a/src/main/java/com/asap/server/service/meeting/dto/BestMeetingTimeDto.java b/src/main/java/com/asap/server/service/meeting/dto/BestMeetingTimeDto.java new file mode 100644 index 00000000..86a1daf9 --- /dev/null +++ b/src/main/java/com/asap/server/service/meeting/dto/BestMeetingTimeDto.java @@ -0,0 +1,30 @@ +package com.asap.server.service.meeting.dto; + +import com.asap.server.presentation.controller.dto.response.MeetingTimeResponseDto; +import com.asap.server.service.time.vo.BestMeetingTimeWithUsers; +import java.util.Arrays; +import java.util.List; + +public record BestMeetingTimeDto( + int memberCount, + MeetingTimeResponseDto bestDateTime, + List otherDateTimes +) { + private static final int FIRST_BEST_MEETING_INDEX = 0; + private static final int SECOND_BEST_MEETING_INDEX = 1; + private static final int THIRD_BEST_MEETING_INDEX = 2; + + public static BestMeetingTimeDto of( + final int memberCount, + final List bestMeetingTimes + ) { + return new BestMeetingTimeDto( + memberCount, + MeetingTimeResponseDto.of(bestMeetingTimes.get(FIRST_BEST_MEETING_INDEX)), + Arrays.asList( + MeetingTimeResponseDto.of(bestMeetingTimes.get(SECOND_BEST_MEETING_INDEX)), + MeetingTimeResponseDto.of(bestMeetingTimes.get(THIRD_BEST_MEETING_INDEX)) + ) + ); + } +} diff --git a/src/main/java/com/asap/server/service/meeting/dto/UserDto.java b/src/main/java/com/asap/server/service/meeting/dto/UserDto.java new file mode 100644 index 00000000..fec865dc --- /dev/null +++ b/src/main/java/com/asap/server/service/meeting/dto/UserDto.java @@ -0,0 +1,7 @@ +package com.asap.server.service.meeting.dto; + +public record UserDto( + Long id, + String name +) { +} diff --git a/src/main/java/com/asap/server/service/meeting/recommend/strategy/ContinuousMeetingTimeStrategy.java b/src/main/java/com/asap/server/service/meeting/recommend/strategy/ContinuousMeetingTimeStrategy.java deleted file mode 100644 index 8e4edb08..00000000 --- a/src/main/java/com/asap/server/service/meeting/recommend/strategy/ContinuousMeetingTimeStrategy.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.asap.server.service.meeting.recommend.strategy; - -import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import com.asap.server.service.vo.BestMeetingTimeVo; -import java.util.List; - -public interface ContinuousMeetingTimeStrategy { - List find(List timeBlocks, Duration duration); -} diff --git a/src/main/java/com/asap/server/service/meeting/recommend/MeetingTimeRecommendService.java b/src/main/java/com/asap/server/service/time/MeetingTimeRecommendService.java similarity index 72% rename from src/main/java/com/asap/server/service/meeting/recommend/MeetingTimeRecommendService.java rename to src/main/java/com/asap/server/service/time/MeetingTimeRecommendService.java index c116021e..671aa280 100644 --- a/src/main/java/com/asap/server/service/meeting/recommend/MeetingTimeRecommendService.java +++ b/src/main/java/com/asap/server/service/time/MeetingTimeRecommendService.java @@ -1,15 +1,14 @@ -package com.asap.server.service.meeting.recommend; +package com.asap.server.service.time; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import com.asap.server.service.meeting.recommend.strategy.BestMeetingTimeStrategy; -import com.asap.server.service.meeting.recommend.strategy.ContinuousMeetingTimeStrategy; -import com.asap.server.service.meeting.recommend.strategy.MeetingTimeCasesStrategy; -import com.asap.server.service.vo.BestMeetingTimeVo; -import com.asap.server.service.vo.PossibleTimeCaseVo; +import com.asap.server.service.time.strategy.BestMeetingTimeStrategy; +import com.asap.server.service.time.strategy.ContinuousMeetingTimeStrategy; +import com.asap.server.service.time.strategy.MeetingTimeCasesStrategy; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.time.vo.BestMeetingTimeVo; +import com.asap.server.service.time.vo.PossibleTimeCaseVo; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -22,7 +21,7 @@ public class MeetingTimeRecommendService { private final BestMeetingTimeStrategy bestMeetingTimeStrategy; public List getBestMeetingTime( - List timeBlocks, + List timeBlocks, final Duration duration, final int userCount ) { @@ -30,8 +29,8 @@ public List getBestMeetingTime( List bestMeetingTimes = new ArrayList<>(); for (PossibleTimeCaseVo timeCase : timeCases) { - List timeBlocksFilteredUserCount = timeBlocks.stream() - .filter(t -> t.userCount() == timeCase.memberCnt()) + List timeBlocksFilteredUserCount = timeBlocks.stream() + .filter(t -> t.userIds().size() == timeCase.memberCnt()) .toList(); List candidateMeetingTimes = @@ -59,7 +58,7 @@ public List getBestMeetingTime( return bestMeetingTimes; } - private boolean isRecommendedMeetingTime(TimeBlockDto timeBlock, BestMeetingTimeVo bestMeetingTime) { + private boolean isRecommendedMeetingTime(TimeBlockVo timeBlock, BestMeetingTimeVo bestMeetingTime) { return timeBlock.availableDate().isEqual(bestMeetingTime.date()) && bestMeetingTime.startTime().getIndex() <= timeBlock.timeSlot().getIndex() && bestMeetingTime.endTime().getIndex() >= timeBlock.timeSlot().getIndex(); diff --git a/src/main/java/com/asap/server/service/time/UserMeetingScheduleService.java b/src/main/java/com/asap/server/service/time/UserMeetingScheduleService.java new file mode 100644 index 00000000..dce64191 --- /dev/null +++ b/src/main/java/com/asap/server/service/time/UserMeetingScheduleService.java @@ -0,0 +1,115 @@ +package com.asap.server.service.time; + +import com.asap.server.common.exception.Error; +import com.asap.server.common.exception.model.BadRequestException; +import com.asap.server.common.utils.DateUtil; +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.persistence.domain.time.UserMeetingSchedule; +import com.asap.server.persistence.repository.UserMeetingScheduleRepository; +import com.asap.server.service.time.dto.register.UserMeetingScheduleRegisterDto; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.time.vo.UserScheduleByTimeSlotVo; +import com.asap.server.service.time.vo.UserScheduleByTimeSlotVo.CompositeKey; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserMeetingScheduleService { + private final UserMeetingScheduleRepository userMeetingScheduleRepository; + + @Transactional + public void createUserMeetingSchedule( + final long meetingId, + final long userId, + final List availableDates + ) { + isDuplicatedDate(availableDates); + + availableDates.forEach(availableDate -> { + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule.builder() + .userId(userId) + .meetingId(meetingId) + .availableDate(DateUtil.transformLocalDate(availableDate.month(), availableDate.day())) + .startTimeSlot(availableDate.startTime()) + .endTimeSlot(availableDate.endTime()) + .weight(availableDate.priority()) + .build(); + + userMeetingScheduleRepository.save(userMeetingSchedule); + } + ); + } + + @Transactional(readOnly = true) + public List getTimeBlocks(final Long meetingId) { + List userMeetingSchedules = userMeetingScheduleRepository.findAllByMeetingId(meetingId); + + return userMeetingSchedules.stream() + .flatMap(this::convertToUserScheduleByTimeSlot) + .collect(Collectors.groupingBy(UserScheduleByTimeSlotVo::composeKey)) + .entrySet().stream() + .map(this::convertToTimeBlock) + .sorted() + .toList(); + } + + public boolean isEmptyHostTimeBlock(final long hostId) { + return userMeetingScheduleRepository.countAllByUserId(hostId) == 0; + } + + private void isDuplicatedDate(final List registerDto) { + Map> meetingTimeAvailable = new HashMap<>(); + for (UserMeetingScheduleRegisterDto requestDto : registerDto) { + String col = String.format("%s %s", requestDto.month(), requestDto.day()); + List timeSlots = TimeSlot.getTimeSlots(requestDto.startTime().ordinal(), + requestDto.endTime().ordinal() - 1); + if (meetingTimeAvailable.containsKey(col)) { + if (meetingTimeAvailable.get(col).stream().anyMatch(timeSlots::contains)) { + throw new BadRequestException(Error.DUPLICATED_TIME_EXCEPTION); + } + } else { + meetingTimeAvailable.put(col, timeSlots); + } + } + } + + private Stream convertToUserScheduleByTimeSlot( + final UserMeetingSchedule userMeetingSchedule + ) { + return TimeSlot.getTimeSlots( + userMeetingSchedule.getStartTimeSlot().getIndex(), + userMeetingSchedule.getEndTimeSlot().getIndex() - 1 + ) + .stream() + .map(timeSlot -> new UserScheduleByTimeSlotVo( + userMeetingSchedule.getId(), + userMeetingSchedule.getAvailableDate(), + userMeetingSchedule.getUserId(), + timeSlot, + userMeetingSchedule.getWeight() + ) + ); + } + + private TimeBlockVo convertToTimeBlock( + final Entry> entry + ) { + List userIds = entry.getValue().stream() + .map(UserScheduleByTimeSlotVo::userId) + .toList(); + + int weight = entry.getValue().stream() + .mapToInt(UserScheduleByTimeSlotVo::weight) + .sum(); + + return new TimeBlockVo(entry.getKey().availableDate(), entry.getKey().time(), weight, userIds); + } +} diff --git a/src/main/java/com/asap/server/service/time/dto/register/UserMeetingScheduleRegisterDto.java b/src/main/java/com/asap/server/service/time/dto/register/UserMeetingScheduleRegisterDto.java new file mode 100644 index 00000000..f51031b2 --- /dev/null +++ b/src/main/java/com/asap/server/service/time/dto/register/UserMeetingScheduleRegisterDto.java @@ -0,0 +1,12 @@ +package com.asap.server.service.time.dto.register; + +import com.asap.server.persistence.domain.enums.TimeSlot; + +public record UserMeetingScheduleRegisterDto( + String month, + String day, + TimeSlot startTime, + TimeSlot endTime, + int priority +) { +} diff --git a/src/main/java/com/asap/server/service/time/dto/register/UserTimeRegisterDto.java b/src/main/java/com/asap/server/service/time/dto/register/UserTimeRegisterDto.java new file mode 100644 index 00000000..dde9f91e --- /dev/null +++ b/src/main/java/com/asap/server/service/time/dto/register/UserTimeRegisterDto.java @@ -0,0 +1,9 @@ +package com.asap.server.service.time.dto.register; + +import java.util.List; + +public record UserTimeRegisterDto( + String name, + List availableSchedules +) { +} diff --git a/src/main/java/com/asap/server/service/time/dto/retrieve/AvailableDatesRetrieveDto.java b/src/main/java/com/asap/server/service/time/dto/retrieve/AvailableDatesRetrieveDto.java new file mode 100644 index 00000000..8dbab20f --- /dev/null +++ b/src/main/java/com/asap/server/service/time/dto/retrieve/AvailableDatesRetrieveDto.java @@ -0,0 +1,21 @@ +package com.asap.server.service.time.dto.retrieve; + +import com.asap.server.common.utils.DateUtil; + +import java.time.LocalDate; +import java.util.List; + +public record AvailableDatesRetrieveDto( + String month, + String day, + String dayOfWeek, + List timeSlots +) { + public static AvailableDatesRetrieveDto of(final LocalDate date, final List timeSlots) { + return new AvailableDatesRetrieveDto( + DateUtil.getMonth(date), + DateUtil.getDay(date), + DateUtil.getDayOfWeek(date), + timeSlots); + } +} diff --git a/src/main/java/com/asap/server/service/time/dto/retrieve/TimeBlockRetrieveDto.java b/src/main/java/com/asap/server/service/time/dto/retrieve/TimeBlockRetrieveDto.java new file mode 100644 index 00000000..c9008fa0 --- /dev/null +++ b/src/main/java/com/asap/server/service/time/dto/retrieve/TimeBlockRetrieveDto.java @@ -0,0 +1,10 @@ +package com.asap.server.service.time.dto.retrieve; + +import java.util.List; + +public record TimeBlockRetrieveDto( + String time, + List userNames, + int colorLevel +) { +} \ No newline at end of file diff --git a/src/main/java/com/asap/server/service/time/dto/retrieve/TimeTableRetrieveDto.java b/src/main/java/com/asap/server/service/time/dto/retrieve/TimeTableRetrieveDto.java new file mode 100644 index 00000000..66463ddb --- /dev/null +++ b/src/main/java/com/asap/server/service/time/dto/retrieve/TimeTableRetrieveDto.java @@ -0,0 +1,14 @@ +package com.asap.server.service.time.dto.retrieve; + + +import java.util.List; + +public record TimeTableRetrieveDto( + int memberCount, + List totalUserNames, + List availableDateTimes +) { + public static TimeTableRetrieveDto of(final List totalUserNames, final List availableDateTimes) { + return new TimeTableRetrieveDto(totalUserNames.size(), totalUserNames, availableDateTimes); + } +} diff --git a/src/main/java/com/asap/server/service/meeting/recommend/strategy/BestMeetingTimeStrategy.java b/src/main/java/com/asap/server/service/time/strategy/BestMeetingTimeStrategy.java similarity index 66% rename from src/main/java/com/asap/server/service/meeting/recommend/strategy/BestMeetingTimeStrategy.java rename to src/main/java/com/asap/server/service/time/strategy/BestMeetingTimeStrategy.java index ba2378d2..11b28865 100644 --- a/src/main/java/com/asap/server/service/meeting/recommend/strategy/BestMeetingTimeStrategy.java +++ b/src/main/java/com/asap/server/service/time/strategy/BestMeetingTimeStrategy.java @@ -1,7 +1,7 @@ -package com.asap.server.service.meeting.recommend.strategy; +package com.asap.server.service.time.strategy; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.service.vo.BestMeetingTimeVo; +import com.asap.server.service.time.vo.BestMeetingTimeVo; import java.util.List; public interface BestMeetingTimeStrategy { diff --git a/src/main/java/com/asap/server/service/time/strategy/ContinuousMeetingTimeStrategy.java b/src/main/java/com/asap/server/service/time/strategy/ContinuousMeetingTimeStrategy.java new file mode 100644 index 00000000..8dad06f1 --- /dev/null +++ b/src/main/java/com/asap/server/service/time/strategy/ContinuousMeetingTimeStrategy.java @@ -0,0 +1,10 @@ +package com.asap.server.service.time.strategy; + +import com.asap.server.persistence.domain.enums.Duration; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.time.vo.BestMeetingTimeVo; +import java.util.List; + +public interface ContinuousMeetingTimeStrategy { + List find(List timeBlocks, Duration duration); +} diff --git a/src/main/java/com/asap/server/service/meeting/recommend/strategy/MeetingTimeCasesStrategy.java b/src/main/java/com/asap/server/service/time/strategy/MeetingTimeCasesStrategy.java similarity index 64% rename from src/main/java/com/asap/server/service/meeting/recommend/strategy/MeetingTimeCasesStrategy.java rename to src/main/java/com/asap/server/service/time/strategy/MeetingTimeCasesStrategy.java index 49a4f332..4eb22221 100644 --- a/src/main/java/com/asap/server/service/meeting/recommend/strategy/MeetingTimeCasesStrategy.java +++ b/src/main/java/com/asap/server/service/time/strategy/MeetingTimeCasesStrategy.java @@ -1,7 +1,7 @@ -package com.asap.server.service.meeting.recommend.strategy; +package com.asap.server.service.time.strategy; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.service.vo.PossibleTimeCaseVo; +import com.asap.server.service.time.vo.PossibleTimeCaseVo; import java.util.List; public interface MeetingTimeCasesStrategy { diff --git a/src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/BestMeetingTimeStrategyImpl.java b/src/main/java/com/asap/server/service/time/strategy/impl/BestMeetingTimeStrategyImpl.java similarity index 84% rename from src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/BestMeetingTimeStrategyImpl.java rename to src/main/java/com/asap/server/service/time/strategy/impl/BestMeetingTimeStrategyImpl.java index 29c73c65..8df6e980 100644 --- a/src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/BestMeetingTimeStrategyImpl.java +++ b/src/main/java/com/asap/server/service/time/strategy/impl/BestMeetingTimeStrategyImpl.java @@ -1,9 +1,9 @@ -package com.asap.server.service.meeting.recommend.strategy.impl; +package com.asap.server.service.time.strategy.impl; import com.asap.server.persistence.domain.enums.Duration; import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.service.meeting.recommend.strategy.BestMeetingTimeStrategy; -import com.asap.server.service.vo.BestMeetingTimeVo; +import com.asap.server.service.time.strategy.BestMeetingTimeStrategy; +import com.asap.server.service.time.vo.BestMeetingTimeVo; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; @@ -27,12 +27,12 @@ public List find(List candidateMeetingTime private BestMeetingTimeVo createFirstMeetingTime(BestMeetingTimeVo candidate, Duration duration) { TimeSlot endTimeSlot = TimeSlot.getTimeSlot(candidate.startTime().getIndex() + duration.getNeedBlock()); - return new BestMeetingTimeVo(candidate.date(), candidate.startTime(), endTimeSlot, candidate.weight()); + return new BestMeetingTimeVo(candidate.date(), candidate.startTime(), endTimeSlot, candidate.weight(), candidate.userIds()); } private BestMeetingTimeVo createSecondMeetingTime(BestMeetingTimeVo candidate, Duration duration) { TimeSlot startTimeSlot = TimeSlot.getTimeSlot(candidate.endTime().getIndex() - duration.getNeedBlock()); - return new BestMeetingTimeVo(candidate.date(), startTimeSlot, candidate.endTime(), candidate.weight()); + return new BestMeetingTimeVo(candidate.date(), startTimeSlot, candidate.endTime(), candidate.weight(), candidate.userIds()); } private boolean isTimeBlockSufficientlyLong(BestMeetingTimeVo candidate, Duration duration) { diff --git a/src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/ContinuousMeetingTimeStrategyImpl.java b/src/main/java/com/asap/server/service/time/strategy/impl/ContinuousMeetingTimeStrategyImpl.java similarity index 66% rename from src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/ContinuousMeetingTimeStrategyImpl.java rename to src/main/java/com/asap/server/service/time/strategy/impl/ContinuousMeetingTimeStrategyImpl.java index 286088c7..68e0f176 100644 --- a/src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/ContinuousMeetingTimeStrategyImpl.java +++ b/src/main/java/com/asap/server/service/time/strategy/impl/ContinuousMeetingTimeStrategyImpl.java @@ -1,19 +1,18 @@ -package com.asap.server.service.meeting.recommend.strategy.impl; +package com.asap.server.service.time.strategy.impl; import com.asap.server.persistence.domain.enums.Duration; import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import com.asap.server.service.meeting.recommend.strategy.ContinuousMeetingTimeStrategy; -import com.asap.server.service.vo.BestMeetingTimeVo; +import com.asap.server.service.time.strategy.ContinuousMeetingTimeStrategy; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.time.vo.BestMeetingTimeVo; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import org.springframework.stereotype.Component; @Component public class ContinuousMeetingTimeStrategyImpl implements ContinuousMeetingTimeStrategy { @Override - public List find(List timeBlocks, Duration duration) { + public List find(List timeBlocks, Duration duration) { List response = new ArrayList<>(); if (timeBlocks.isEmpty()) { return response; @@ -23,8 +22,8 @@ public List find(List timeBlocks, Duration dura int endIdx = 1; while (endIdx < timeBlocks.size()) { - TimeBlockDto endTimeBlock = timeBlocks.get(endIdx - 1); - TimeBlockDto nextTimeBlock = timeBlocks.get(endIdx); + TimeBlockVo endTimeBlock = timeBlocks.get(endIdx - 1); + TimeBlockVo nextTimeBlock = timeBlocks.get(endIdx); if (isContinuous(endTimeBlock, nextTimeBlock)) { endIdx++; @@ -57,39 +56,41 @@ public List find(List timeBlocks, Duration dura } private void validateAndAddMeetingTime( - List timeBlocks, + List timeBlocks, Duration duration, int startIdx, int endIdx, List response ) { - TimeBlockDto startTimeBlock = timeBlocks.get(startIdx); - TimeBlockDto endTimeBlock = timeBlocks.get(endIdx - 1); + TimeBlockVo startTimeBlock = timeBlocks.get(startIdx); + TimeBlockVo endTimeBlock = timeBlocks.get(endIdx - 1); if (isSatisfiedDuration(startTimeBlock, endTimeBlock, duration)) { int weight = sumTimeBlocksWeight(timeBlocks, startIdx, endIdx); + List userIds = findUserIdsBetween(timeBlocks, startIdx, endIdx); TimeSlot endTimeSlot = TimeSlot.getTimeSlot(endTimeBlock.timeSlot().getIndex() + 1); response.add( new BestMeetingTimeVo( startTimeBlock.availableDate(), startTimeBlock.timeSlot(), endTimeSlot, - weight + weight, + userIds ) ); } } private boolean isContinuous( - TimeBlockDto endTimeBlock, - TimeBlockDto nextTimeBlock + TimeBlockVo endTimeBlock, + TimeBlockVo nextTimeBlock ) { return endTimeBlock.availableDate().isEqual(nextTimeBlock.availableDate()) && endTimeBlock.timeSlot().getIndex() + 1 == nextTimeBlock.timeSlot().getIndex(); } private boolean isSatisfiedDuration( - TimeBlockDto startTimeBlock, - TimeBlockDto endTimeBlock, + TimeBlockVo startTimeBlock, + TimeBlockVo endTimeBlock, Duration duration ) { int blockCnt = endTimeBlock.timeSlot().getIndex() - startTimeBlock.timeSlot().getIndex(); @@ -97,13 +98,24 @@ private boolean isSatisfiedDuration( } private int sumTimeBlocksWeight( - final List timeBlocks, + final List timeBlocks, final int startIdx, final int endIdx ) { int totalWeight = timeBlocks.subList(startIdx, endIdx).stream() - .mapToInt(TimeBlockDto::weight) + .mapToInt(TimeBlockVo::weight) .sum(); return totalWeight / (endIdx - startIdx); } + + private List findUserIdsBetween( + final List timeBlocks, + final int startIdx, + final int endIdx + ) { + return timeBlocks.subList(startIdx, endIdx).stream() + .flatMap(timeBlock -> timeBlock.userIds().stream()) + .distinct() + .toList(); + } } diff --git a/src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/MeetingTimeCasesStrategyImpl.java b/src/main/java/com/asap/server/service/time/strategy/impl/MeetingTimeCasesStrategyImpl.java similarity index 96% rename from src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/MeetingTimeCasesStrategyImpl.java rename to src/main/java/com/asap/server/service/time/strategy/impl/MeetingTimeCasesStrategyImpl.java index 989026b0..62ef4c4a 100644 --- a/src/main/java/com/asap/server/service/meeting/recommend/strategy/impl/MeetingTimeCasesStrategyImpl.java +++ b/src/main/java/com/asap/server/service/time/strategy/impl/MeetingTimeCasesStrategyImpl.java @@ -1,8 +1,8 @@ -package com.asap.server.service.meeting.recommend.strategy.impl; +package com.asap.server.service.time.strategy.impl; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.service.meeting.recommend.strategy.MeetingTimeCasesStrategy; -import com.asap.server.service.vo.PossibleTimeCaseVo; +import com.asap.server.service.time.strategy.MeetingTimeCasesStrategy; +import com.asap.server.service.time.vo.PossibleTimeCaseVo; import org.springframework.stereotype.Component; import java.util.ArrayList; diff --git a/src/main/java/com/asap/server/service/vo/BestMeetingTimeVo.java b/src/main/java/com/asap/server/service/time/vo/BestMeetingTimeVo.java similarity index 56% rename from src/main/java/com/asap/server/service/vo/BestMeetingTimeVo.java rename to src/main/java/com/asap/server/service/time/vo/BestMeetingTimeVo.java index 18c1592e..47430dc1 100644 --- a/src/main/java/com/asap/server/service/vo/BestMeetingTimeVo.java +++ b/src/main/java/com/asap/server/service/time/vo/BestMeetingTimeVo.java @@ -1,8 +1,9 @@ -package com.asap.server.service.vo; +package com.asap.server.service.time.vo; import com.asap.server.persistence.domain.enums.TimeSlot; import java.time.LocalDate; +import java.util.List; -public record BestMeetingTimeVo(LocalDate date, TimeSlot startTime, TimeSlot endTime, int weight) { +public record BestMeetingTimeVo(LocalDate date, TimeSlot startTime, TimeSlot endTime, int weight, List userIds) { } diff --git a/src/main/java/com/asap/server/service/vo/BestMeetingTimeWithUsersVo.java b/src/main/java/com/asap/server/service/time/vo/BestMeetingTimeWithUsers.java similarity index 63% rename from src/main/java/com/asap/server/service/vo/BestMeetingTimeWithUsersVo.java rename to src/main/java/com/asap/server/service/time/vo/BestMeetingTimeWithUsers.java index 9b46afe2..5390354a 100644 --- a/src/main/java/com/asap/server/service/vo/BestMeetingTimeWithUsersVo.java +++ b/src/main/java/com/asap/server/service/time/vo/BestMeetingTimeWithUsers.java @@ -1,22 +1,23 @@ -package com.asap.server.service.vo; +package com.asap.server.service.time.vo; import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.service.meeting.dto.UserDto; import java.time.LocalDate; import java.util.List; -public record BestMeetingTimeWithUsersVo( +public record BestMeetingTimeWithUsers( LocalDate date, TimeSlot startTime, TimeSlot endTime, int weight, - List users + List users ) { - public static BestMeetingTimeWithUsersVo of( + public static BestMeetingTimeWithUsers of( final BestMeetingTimeVo bestMeetingTimeVo, - final List users + final List users ) { - return new BestMeetingTimeWithUsersVo( + return new BestMeetingTimeWithUsers( bestMeetingTimeVo.date(), bestMeetingTimeVo.startTime(), bestMeetingTimeVo.endTime(), diff --git a/src/main/java/com/asap/server/service/vo/PossibleTimeCaseVo.java b/src/main/java/com/asap/server/service/time/vo/PossibleTimeCaseVo.java similarity index 76% rename from src/main/java/com/asap/server/service/vo/PossibleTimeCaseVo.java rename to src/main/java/com/asap/server/service/time/vo/PossibleTimeCaseVo.java index 78e9c733..43916489 100644 --- a/src/main/java/com/asap/server/service/vo/PossibleTimeCaseVo.java +++ b/src/main/java/com/asap/server/service/time/vo/PossibleTimeCaseVo.java @@ -1,4 +1,4 @@ -package com.asap.server.service.vo; +package com.asap.server.service.time.vo; import com.asap.server.persistence.domain.enums.Duration; diff --git a/src/main/java/com/asap/server/service/time/vo/TimeBlockVo.java b/src/main/java/com/asap/server/service/time/vo/TimeBlockVo.java new file mode 100644 index 00000000..1fe7350d --- /dev/null +++ b/src/main/java/com/asap/server/service/time/vo/TimeBlockVo.java @@ -0,0 +1,21 @@ +package com.asap.server.service.time.vo; + +import com.asap.server.persistence.domain.enums.TimeSlot; +import java.time.LocalDate; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +public record TimeBlockVo( + LocalDate availableDate, + TimeSlot timeSlot, + int weight, + List userIds +) implements Comparable { + @Override + public int compareTo(@NotNull final TimeBlockVo o) { + if (this.availableDate.equals(o.availableDate)) { + return Integer.compare(this.timeSlot.getIndex(), o.timeSlot.getIndex()); + } + return this.availableDate.compareTo(o.availableDate); + } +} diff --git a/src/main/java/com/asap/server/service/time/vo/UserScheduleByTimeSlotVo.java b/src/main/java/com/asap/server/service/time/vo/UserScheduleByTimeSlotVo.java new file mode 100644 index 00000000..994e2fb5 --- /dev/null +++ b/src/main/java/com/asap/server/service/time/vo/UserScheduleByTimeSlotVo.java @@ -0,0 +1,19 @@ +package com.asap.server.service.time.vo; + +import com.asap.server.persistence.domain.enums.TimeSlot; +import java.time.LocalDate; + +public record UserScheduleByTimeSlotVo( + Long id, + LocalDate availableDate, + Long userId, + TimeSlot time, + int weight +) { + public record CompositeKey(LocalDate availableDate, TimeSlot time) { + } + + public CompositeKey composeKey() { + return new CompositeKey(this.availableDate, this.time); + } +} diff --git a/src/main/java/com/asap/server/service/user/UserLoginService.java b/src/main/java/com/asap/server/service/user/UserLoginService.java new file mode 100644 index 00000000..a6d3f651 --- /dev/null +++ b/src/main/java/com/asap/server/service/user/UserLoginService.java @@ -0,0 +1,59 @@ +package com.asap.server.service.user; + +import static com.asap.server.common.exception.Error.MEETING_VALIDATION_FAILED_EXCEPTION; + +import com.asap.server.common.exception.Error; +import com.asap.server.common.exception.model.ConflictException; +import com.asap.server.common.exception.model.HostTimeForbiddenException; +import com.asap.server.common.exception.model.NotFoundException; +import com.asap.server.common.exception.model.UnauthorizedException; +import com.asap.server.common.jwt.JwtService; +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.repository.meeting.MeetingRepository; +import com.asap.server.service.time.UserMeetingScheduleService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserLoginService { + private final MeetingRepository meetingRepository; + private final UserMeetingScheduleService userMeetingScheduleService; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + + @Transactional + public String loginByHost( + final Long meetingId, + final String name, + final String password + ) { + Meeting meeting = meetingRepository.findByIdWithHost(meetingId) + .orElseThrow(() -> new NotFoundException(Error.MEETING_NOT_FOUND_EXCEPTION)); + + Name hostName = new Name(name); + + if (!meeting.checkHostName(hostName)) { + throw new UnauthorizedException(Error.INVALID_HOST_ID_PASSWORD_EXCEPTION); + } + + if (!passwordEncoder.matches(password, meeting.getPassword())) { + throw new UnauthorizedException(Error.INVALID_HOST_ID_PASSWORD_EXCEPTION); + } + + if (meeting.isConfirmedMeeting()) { + throw new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION); + } + + String hostAccessToken = jwtService.issuedToken(meeting.getHost().getId().toString()); + + if (userMeetingScheduleService.isEmptyHostTimeBlock(meeting.getHost().getId())) { + throw new HostTimeForbiddenException(Error.HOST_MEETING_TIME_NOT_PROVIDED, hostAccessToken); + } + + return hostAccessToken; + } +} diff --git a/src/main/java/com/asap/server/service/user/UserRetrieveService.java b/src/main/java/com/asap/server/service/user/UserRetrieveService.java new file mode 100644 index 00000000..3f8b4bba --- /dev/null +++ b/src/main/java/com/asap/server/service/user/UserRetrieveService.java @@ -0,0 +1,28 @@ +package com.asap.server.service.user; + +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.user.User; +import com.asap.server.persistence.repository.user.UserRepository; + +import java.util.Map; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserRetrieveService { + private final UserRepository userRepository; + + public int getMeetingUserCount(final Meeting meeting) { + return userRepository.countByMeeting(meeting); + } + + public Map getUserIdToUserMap(final Long meetingId) { + return userRepository + .findAllByMeetingId(meetingId).stream() + .collect(Collectors.toMap(User::getId, user -> user)); + } +} diff --git a/src/main/java/com/asap/server/service/vo/UserVo.java b/src/main/java/com/asap/server/service/vo/UserVo.java deleted file mode 100644 index f7ce13f8..00000000 --- a/src/main/java/com/asap/server/service/vo/UserVo.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.asap.server.service.vo; - -import com.querydsl.core.annotations.QueryProjection; - -public record UserVo( - Long id, - String name -) { - @QueryProjection - public UserVo { - } -} diff --git a/src/test/java/com/asap/server/common/generator/TimeBlockDtoGenerator.java b/src/test/java/com/asap/server/common/generator/TimeBlockDtoGenerator.java deleted file mode 100644 index 19f1f74d..00000000 --- a/src/test/java/com/asap/server/common/generator/TimeBlockDtoGenerator.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.asap.server.common.generator; - -import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; - -public class TimeBlockDtoGenerator { - private static final TimeSlot[] timeSlots = TimeSlot.values(); - - public static List generator( - LocalDate availableDate, - TimeSlot startTime, - TimeSlot endTime, - int weight, - long userCount - ) { - List timeBlocks = new ArrayList<>(); - for (int i = startTime.ordinal(); i <= endTime.ordinal(); i++) { - timeBlocks.add(new TimeBlockDto(availableDate, timeSlots[i], weight, userCount)); - } - return timeBlocks; - } -} diff --git a/src/test/java/com/asap/server/concurrency/DuplicatedInterceptorTest.java b/src/test/java/com/asap/server/concurrency/DuplicatedInterceptorTest.java index 9575eef9..b183f334 100644 --- a/src/test/java/com/asap/server/concurrency/DuplicatedInterceptorTest.java +++ b/src/test/java/com/asap/server/concurrency/DuplicatedInterceptorTest.java @@ -20,6 +20,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -27,6 +28,7 @@ @SpringBootTest @AutoConfigureMockMvc +@Transactional public class DuplicatedInterceptorTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/asap/server/persistence/domain/user/NameTest.java b/src/test/java/com/asap/server/persistence/domain/user/NameTest.java new file mode 100644 index 00000000..38ae54d7 --- /dev/null +++ b/src/test/java/com/asap/server/persistence/domain/user/NameTest.java @@ -0,0 +1,56 @@ +package com.asap.server.persistence.domain.user; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.asap.server.common.exception.model.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + + @DisplayName("이름 값이 null로 들어오면 BadRequestException 을 반환한다.") + @Test + void test() { + // when, then + assertThatThrownBy(() -> { + new Name(null); + }).isInstanceOf(BadRequestException.class) + .hasMessage("사용자 이름에는 null이 들어올 수 없습니다."); + } + + @DisplayName("이름 값이 공백으로 들어오면 BadRequestException 을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"", " ", " "}) + void test2(String value) { + // when, then + assertThatThrownBy(() -> { + new Name(value); + }).isInstanceOf(BadRequestException.class) + .hasMessage("사용자 이름에는 빈 값이 들어올 수 없습니다."); + } + + @DisplayName("이름 값이 8자를 초과하면 BadRequestException 을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"KWY이름8자초과", "DSH이름8자초과"}) + void test3(String value) { + // when, then + assertThatThrownBy(() -> { + new Name(value); + }).isInstanceOf(BadRequestException.class) + .hasMessage("사용자 이름의 최대 입력 길이(8자)를 초과했습니다."); + } + + @DisplayName("이름 값 앞 뒤로 공백을 제거한 값을 저장한다.") + @ParameterizedTest + @ValueSource(strings = {" KWY", "KWY ", " KWY "}) + void test4(String value) { + // when + Name name = new Name(value); + + // then + assertThat(name.getValue()).isEqualTo("KWY"); + } +} \ No newline at end of file diff --git a/src/test/java/com/asap/server/persistence/repository/MeetingRepositoryCustomTest.java b/src/test/java/com/asap/server/persistence/repository/MeetingRepositoryCustomTest.java index c2c81cec..6f501cec 100644 --- a/src/test/java/com/asap/server/persistence/repository/MeetingRepositoryCustomTest.java +++ b/src/test/java/com/asap/server/persistence/repository/MeetingRepositoryCustomTest.java @@ -3,7 +3,8 @@ import com.asap.server.persistence.config.querydsl.QueryDslConfig; import com.asap.server.persistence.domain.Meeting; import com.asap.server.persistence.domain.Place; -import com.asap.server.persistence.domain.User; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; import com.asap.server.persistence.domain.enums.Duration; import com.asap.server.persistence.domain.enums.PlaceType; import com.asap.server.persistence.domain.enums.Role; @@ -44,9 +45,10 @@ void fetchJoinTest() { .place(place) .build(); + final Name name = new Name("강원용"); final User user = User.builder() .meeting(meeting) - .name("강원용") + .name(name) .role(Role.HOST) .isFixed(false) .build(); diff --git a/src/test/java/com/asap/server/persistence/repository/internal/MetricsRepositoryGenerateDateFilterTest.java b/src/test/java/com/asap/server/persistence/repository/internal/MetricsRepositoryGenerateDateFilterTest.java new file mode 100644 index 00000000..5d94588e --- /dev/null +++ b/src/test/java/com/asap/server/persistence/repository/internal/MetricsRepositoryGenerateDateFilterTest.java @@ -0,0 +1,107 @@ +package com.asap.server.persistence.repository.internal; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import jakarta.persistence.EntityManager; +import java.time.LocalDateTime; +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.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@SpringBootTest +class MetricsRepositoryGenerateDateFilterTest { + private static final String INSERT_QUERY_TEMPLATE = "INSERT INTO meeting (title, password, duration, place_type, additional_info, created_at) VALUES ('title', '1234', 'HALF','ONLINE', '', ?)"; + + @Autowired + private MetricsRepository metricsRepository; + + @Autowired + private EntityManager em; + + @DisplayName( + """ + 1. 2022년 5월 20일 데이터 + 2. 2023년 1월 1일 데이터 + 3. 2023년 6월 15일 데이터 + 4. 2023년 12월 31일 데이터 + """ + ) + @BeforeEach + public void setUp() { + em.createNativeQuery(INSERT_QUERY_TEMPLATE) + .setParameter(1, "2022-05-20T10:00:00") + .executeUpdate(); + + em.createNativeQuery(INSERT_QUERY_TEMPLATE) + .setParameter(1, "2023-01-01T12:00:00") + .executeUpdate(); + + em.createNativeQuery(INSERT_QUERY_TEMPLATE) + .setParameter(1, "2023-06-15T15:30:00") + .executeUpdate(); + + em.createNativeQuery(INSERT_QUERY_TEMPLATE) + .setParameter(1, "2023-12-31T23:59:59") + .executeUpdate(); + } + + @DisplayName("시작 날짜와 종료 날짜 모두가 주어졌을 때, 주어진 범위 내의 결과를 반환한다.") + @Test + public void test() { + // given + LocalDateTime from = LocalDateTime.of(2023, 1, 1, 0, 0); + LocalDateTime to = LocalDateTime.of(2023, 12, 31, 0, 0); + + // when + Long count = metricsRepository.countTotalMeetingCount(from, to); + + // then + assertThat(count).isEqualTo(2); + } + + @DisplayName("시작 날짜가 주어지고 종료 날짜가 주어지지 않았을 때, 시작 날짜 이후의 결과를 반환한다.") + @Test + public void test2() { + // given + LocalDateTime from = LocalDateTime.of(2023, 6, 1, 0, 0); + LocalDateTime to = null; + + // when + Long count = metricsRepository.countTotalMeetingCount(from, to); + + // then + assertThat(count).isEqualTo(2); + } + + @DisplayName("종료 날짜가 주어지고 시작 날짜가 주어지지 않았을 때, 종료 날짜 이전의 결과를 반환한다.") + @Test + public void test3() { + // given + LocalDateTime from = null; + LocalDateTime to = LocalDateTime.of(2023, 1, 1, 0, 0); + + // when + Long count = metricsRepository.countTotalMeetingCount(from, to); + + // then + assertThat(count).isEqualTo(1); + } + + @DisplayName("날짜 범위가 주어지지 않았을 때, 전범위를 반환한다.") + @Test + public void test4() { + // given + LocalDateTime from = null; + LocalDateTime to = null; + + // when + Long count = metricsRepository.countTotalMeetingCount(from, to); + + // then + assertThat(count).isEqualTo(4); + } +} diff --git a/src/test/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryImplTest.java b/src/test/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryImplTest.java deleted file mode 100644 index adaf127b..00000000 --- a/src/test/java/com/asap/server/persistence/repository/timeblock/TimeBlockRepositoryImplTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.asap.server.persistence.repository.timeblock; - -import com.asap.server.persistence.config.querydsl.QueryDslConfig; -import com.asap.server.persistence.domain.AvailableDate; -import com.asap.server.persistence.domain.Meeting; -import com.asap.server.persistence.domain.Place; -import com.asap.server.persistence.domain.TimeBlock; -import com.asap.server.persistence.domain.TimeBlockUser; -import com.asap.server.persistence.domain.User; -import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.persistence.domain.enums.PlaceType; -import com.asap.server.persistence.domain.enums.Role; -import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.persistence.repository.timeblock.TimeBlockRepository; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import jakarta.persistence.EntityManager; -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.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -@DataJpaTest -@Import(QueryDslConfig.class) -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -class TimeBlockRepositoryImplTest { - @Autowired - EntityManager em; - @Autowired - TimeBlockRepository timeBlockRepositoryCustom; - - @Test - @DisplayName("2명의 사용자 모두 2개의 요일에 6_00 ~ 8_00까지 시간을 입력했을 때, 총 10개의 레코드를 불러온다.") - void findAllTimeBlockByMeetingTest() { - // given - Place place = Place.builder() - .placeType(PlaceType.OFFLINE) - .build(); - Meeting meeting = Meeting.builder() - .title("회의 테스트") - .password("0000") - .additionalInfo("") - .duration(Duration.HALF) - .place(place) - .build(); - em.persist(meeting); - - User user = User.builder() - .meeting(meeting) - .name("KWY") - .role(Role.HOST) - .build(); - User user2 = User.builder() - .meeting(meeting) - .name("DSY") - .role(Role.MEMBER) - .build(); - em.persist(user); - em.persist(user2); - - AvailableDate availableDate = AvailableDate.builder() - .date(LocalDate.of(2024, 7, 9)) - .meeting(meeting) - .build(); - AvailableDate availableDate2 = AvailableDate.builder() - .date(LocalDate.of(2024, 7, 10)) - .meeting(meeting) - .build(); - em.persist(availableDate); - em.persist(availableDate2); - - List timeBlocks = createTimeBlocks(availableDate, TimeSlot.SLOT_6_00, TimeSlot.SLOT_8_00); - List timeBlocks2 = createTimeBlocks(availableDate2, TimeSlot.SLOT_6_00, TimeSlot.SLOT_8_00); - List tbu = createTimeBlockUsers(timeBlocks, user); - List tbu2 = createTimeBlockUsers(timeBlocks2, user); - List tbu3 = createTimeBlockUsers(timeBlocks, user2); - List tbu4 = createTimeBlockUsers(timeBlocks2, user2); - - // when - List response = timeBlockRepositoryCustom.findAllTimeBlockByMeeting(meeting.getId()); - - // then - assertThat(response.size()).isEqualTo(10); - } - - private List createTimeBlocks(AvailableDate availableDate, TimeSlot startTime, TimeSlot endTime) { - List timeSlots = TimeSlot.getTimeSlots(startTime.ordinal(), endTime.ordinal()); - List timeBlocks = new ArrayList<>(); - for (TimeSlot timeSlot : timeSlots) { - final TimeBlock timeBlock = TimeBlock.builder() - .availableDate(availableDate) - .timeSlot(timeSlot) - .build(); - em.persist(timeBlock); - timeBlocks.add(timeBlock); - } - return timeBlocks; - } - - private List createTimeBlockUsers(List timeBlocks, User user) { - List timeBlockUsers = new ArrayList<>(); - for (TimeBlock timeBlock : timeBlocks) { - TimeBlockUser tbu = TimeBlockUser.builder() - .timeBlock(timeBlock) - .user(user) - .build(); - em.persist(tbu); - timeBlockUsers.add(tbu); - } - return timeBlockUsers; - } -} \ No newline at end of file diff --git a/src/test/java/com/asap/server/persistence/repository/user/UserRepositoryImplTest.java b/src/test/java/com/asap/server/persistence/repository/user/UserRepositoryImplTest.java index 500642a2..bdd7cb2c 100644 --- a/src/test/java/com/asap/server/persistence/repository/user/UserRepositoryImplTest.java +++ b/src/test/java/com/asap/server/persistence/repository/user/UserRepositoryImplTest.java @@ -5,7 +5,8 @@ import com.asap.server.persistence.config.querydsl.QueryDslConfig; import com.asap.server.persistence.domain.Meeting; import com.asap.server.persistence.domain.Place; -import com.asap.server.persistence.domain.User; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; import com.asap.server.persistence.domain.enums.Duration; import com.asap.server.persistence.domain.enums.PlaceType; import com.asap.server.persistence.domain.enums.Role; @@ -43,27 +44,31 @@ void updateUserIsFixedByMeetingTest() { .build(); em.persist(meeting); + final Name name = new Name("KWY"); + final Name name2 = new Name("DSY"); + final Name name3 = new Name("LJH"); + final Name name4 = new Name("SES"); User user = User.builder() .meeting(meeting) - .name("KWY") + .name(name) .isFixed(false) .role(Role.HOST) .build(); User user2 = User.builder() .meeting(meeting) - .name("DSY") + .name(name2) .isFixed(false) .role(Role.MEMBER) .build(); User user3 = User.builder() .meeting(meeting) - .name("LJH") + .name(name3) .isFixed(false) .role(Role.MEMBER) .build(); User user4 = User.builder() .meeting(meeting) - .name("SES") + .name(name4) .isFixed(false) .role(Role.MEMBER) .build(); diff --git a/src/test/java/com/asap/server/presentation/controller/MeetingControllerTest.java b/src/test/java/com/asap/server/presentation/controller/MeetingRetrieveControllerTest.java similarity index 88% rename from src/test/java/com/asap/server/presentation/controller/MeetingControllerTest.java rename to src/test/java/com/asap/server/presentation/controller/MeetingRetrieveControllerTest.java index d9e54c2b..bbb6f58d 100644 --- a/src/test/java/com/asap/server/presentation/controller/MeetingControllerTest.java +++ b/src/test/java/com/asap/server/presentation/controller/MeetingRetrieveControllerTest.java @@ -1,6 +1,6 @@ package com.asap.server.presentation.controller; -import com.asap.server.presentation.controller.MeetingController; +import com.asap.server.presentation.controller.meeting.MeetingRegisterController; import com.asap.server.service.MeetingService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -16,12 +16,14 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.nio.charset.Charset; +import org.springframework.transaction.annotation.Transactional; +@Transactional @ExtendWith(MockitoExtension.class) -public class MeetingControllerTest { +public class MeetingRetrieveControllerTest { @InjectMocks - private MeetingController target; + private MeetingRegisterController target; @Mock private MeetingService meetingService; diff --git a/src/test/java/com/asap/server/presentation/controller/user/CreateUserTimeE2ETest.java b/src/test/java/com/asap/server/presentation/controller/user/CreateUserTimeE2ETest.java new file mode 100644 index 00000000..09a9e4e4 --- /dev/null +++ b/src/test/java/com/asap/server/presentation/controller/user/CreateUserTimeE2ETest.java @@ -0,0 +1,356 @@ +package com.asap.server.presentation.controller.user; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.asap.server.common.jwt.JwtService; +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.Place; +import com.asap.server.persistence.domain.enums.Duration; +import com.asap.server.persistence.domain.enums.PlaceType; +import com.asap.server.persistence.domain.enums.Role; +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.persistence.domain.time.UserMeetingSchedule; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; +import com.asap.server.presentation.common.secure.SecureUrlUtil; +import com.asap.server.presentation.controller.dto.request.AvailableTimeRequestDto; +import com.asap.server.presentation.controller.dto.request.UserMeetingTimeSaveRequestDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@SpringBootTest +@AutoConfigureMockMvc +public class CreateUserTimeE2ETest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private EntityManager em; + @Autowired + private SecureUrlUtil secureUrlUtil; + @Autowired + private JwtService jwtService; + + @Nested + @DisplayName("[멤버] 시간 입력 테스트 코드") + class MemberCreateUserTimeTest { + @Test + @DisplayName("[성공 케이스] 멤버가 회의 시간을 성공적으로 등록할 수 있다.") + void test() throws Exception { + // given + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto2 = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_15_00, TimeSlot.SLOT_16_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto3 = new UserMeetingTimeSaveRequestDto("7", "10", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + List availableTimes = List.of( + userMeetingTimeSaveRequestDto, + userMeetingTimeSaveRequestDto2, + userMeetingTimeSaveRequestDto3 + ); + + Place place = Place.builder() + .placeType(PlaceType.OFFLINE) + .build(); + Meeting meeting = Meeting.builder() + .title("회의 테스트") + .password("0000") + .additionalInfo("") + .duration(Duration.HALF) + .place(place) + .build(); + em.persist(meeting); + String encodedMeetingId = secureUrlUtil.encodeUrl(meeting.getId()); + + AvailableTimeRequestDto availableTimeRequestDto = new AvailableTimeRequestDto("KWY", availableTimes); + String body = objectMapper.writeValueAsString(availableTimeRequestDto); + + // when, then + mockMvc.perform( + post("/user/" + encodedMeetingId + "/time") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ).andExpect(jsonPath("$.code").value(201)) + .andExpect(jsonPath("$.message").value("참여자 회의 가능 시간 입력을 성공하였습니다.")) + .andExpect(jsonPath("$.data.role").value("MEMBER")); + + String jpql = "SELECT ums FROM UserMeetingSchedule ums WHERE ums.meetingId=" + meeting.getId(); + List result = + em.createQuery(jpql, UserMeetingSchedule.class).getResultList(); + + assertThat(result.size()).isEqualTo(3); + } + + @Test + @DisplayName("[오류 케이스] 멤버가 특정 요일에 중복된 시간을 입력하면 400 에러를 반환한다.") + void test2() throws Exception { + // given + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto2 = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_13_00, TimeSlot.SLOT_16_00, 1); + List availableTimes = List.of( + userMeetingTimeSaveRequestDto, + userMeetingTimeSaveRequestDto2 + ); + + Place place = Place.builder() + .placeType(PlaceType.OFFLINE) + .build(); + Meeting meeting = Meeting.builder() + .title("회의 테스트") + .password("0000") + .additionalInfo("") + .duration(Duration.HALF) + .place(place) + .build(); + em.persist(meeting); + String encodedMeetingId = secureUrlUtil.encodeUrl(meeting.getId()); + + AvailableTimeRequestDto availableTimeRequestDto = new AvailableTimeRequestDto("KWY", availableTimes); + String body = objectMapper.writeValueAsString(availableTimeRequestDto); + + // when, then + mockMvc.perform( + post("/user/" + encodedMeetingId + "/time") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ).andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("중복 입력된 시간이 있습니다.")); + } + } + + @Nested + @DisplayName("[방장] 시간 입력 테스트") + class HostCreateUserTimeTest { + @Test + @DisplayName("[성공 케이스] 방장이 회의 시간을 성공적으로 등록할 수 있다.") + void test() throws Exception { + // given + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto2 = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_15_00, TimeSlot.SLOT_16_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto3 = new UserMeetingTimeSaveRequestDto("7", "10", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + List availableTimes = List.of( + userMeetingTimeSaveRequestDto, + userMeetingTimeSaveRequestDto2, + userMeetingTimeSaveRequestDto3 + ); + + Place place = Place.builder() + .placeType(PlaceType.OFFLINE) + .build(); + Meeting meeting = Meeting.builder() + .title("회의 테스트") + .password("0000") + .additionalInfo("") + .duration(Duration.HALF) + .place(place) + .build(); + em.persist(meeting); + + User user = User.builder() + .name(new Name("KWY")) + .meeting(meeting) + .role(Role.HOST) + .build(); + em.persist(user); + meeting.setHost(user); + + String encodedMeetingId = secureUrlUtil.encodeUrl(meeting.getId()); + String hostJwtToken = jwtService.issuedToken(String.valueOf(user.getId())); + + String body = objectMapper.writeValueAsString(availableTimes); + + // when, then + mockMvc.perform( + post("/user/host/" + encodedMeetingId + "/time") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header("Authorization", "Bearer " + hostJwtToken) + ).andExpect(jsonPath("$.code").value(201)) + .andExpect(jsonPath("$.message").value("방장의 회의 가능 시간이 성공적으로 입력되었습니다.")); + + String jpql = "SELECT ums FROM UserMeetingSchedule ums WHERE ums.meetingId=" + meeting.getId(); + List result = + em.createQuery(jpql, UserMeetingSchedule.class).getResultList(); + + assertThat(result.size()).isEqualTo(3); + } + + @Test + @DisplayName("[오류 케이스] 방장이 특정 요일에 중복된 시간을 입력하면 400 에러를 반환한다.") + void test2() throws Exception { + // given + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto2 = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_13_00, TimeSlot.SLOT_16_00, 1); + List availableTimes = List.of( + userMeetingTimeSaveRequestDto, + userMeetingTimeSaveRequestDto2 + ); + + Place place = Place.builder() + .placeType(PlaceType.OFFLINE) + .build(); + Meeting meeting = Meeting.builder() + .title("회의 테스트") + .password("0000") + .additionalInfo("") + .duration(Duration.HALF) + .place(place) + .build(); + em.persist(meeting); + + User user = User.builder() + .name(new Name("KWY")) + .meeting(meeting) + .role(Role.HOST) + .build(); + em.persist(user); + meeting.setHost(user); + + String encodedMeetingId = secureUrlUtil.encodeUrl(meeting.getId()); + String hostJwtToken = jwtService.issuedToken(String.valueOf(user.getId())); + + String body = objectMapper.writeValueAsString(availableTimes); + + // when, then + mockMvc.perform( + post("/user/host/" + encodedMeetingId + "/time") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header("Authorization", "Bearer " + hostJwtToken) + ).andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("중복 입력된 시간이 있습니다.")); + } + + @Test + @DisplayName("[오류 케이스] 요청자가 방장이 아닐 경우 401 에러를 반환한다.") + void test3() throws Exception { + // given + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto2 = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_13_00, TimeSlot.SLOT_16_00, 1); + List availableTimes = List.of( + userMeetingTimeSaveRequestDto, + userMeetingTimeSaveRequestDto2 + ); + + Place place = Place.builder() + .placeType(PlaceType.OFFLINE) + .build(); + Meeting meeting = Meeting.builder() + .title("회의 테스트") + .password("0000") + .additionalInfo("") + .duration(Duration.HALF) + .place(place) + .build(); + em.persist(meeting); + + User user = User.builder() + .name(new Name("KWY")) + .meeting(meeting) + .role(Role.HOST) + .build(); + em.persist(user); + meeting.setHost(user); + + String encodedMeetingId = secureUrlUtil.encodeUrl(meeting.getId()); + String hostJwtToken = jwtService.issuedToken(String.valueOf(user.getId() + 1)); + + String body = objectMapper.writeValueAsString(availableTimes); + + // when, then + mockMvc.perform( + post("/user/host/" + encodedMeetingId + "/time") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header("Authorization", "Bearer " + hostJwtToken) + ).andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.message").value("해당 유저는 해당 방의 방장이 아닙니다.")); + } + + @Test + @DisplayName("[오류 케이스] 방장이 이미 시간을 입력했을 경우, 추가 입력 요청 시 409 에러를 반환한다.") + void test4() throws Exception { + // given + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_9_00, TimeSlot.SLOT_14_00, 1); + UserMeetingTimeSaveRequestDto userMeetingTimeSaveRequestDto2 = new UserMeetingTimeSaveRequestDto("7", "9", + TimeSlot.SLOT_13_00, TimeSlot.SLOT_16_00, 1); + List availableTimes = List.of( + userMeetingTimeSaveRequestDto, + userMeetingTimeSaveRequestDto2 + ); + + Place place = Place.builder() + .placeType(PlaceType.OFFLINE) + .build(); + Meeting meeting = Meeting.builder() + .title("회의 테스트") + .password("0000") + .additionalInfo("") + .duration(Duration.HALF) + .place(place) + .build(); + em.persist(meeting); + + User user = User.builder() + .name(new Name("KWY")) + .meeting(meeting) + .role(Role.HOST) + .build(); + em.persist(user); + meeting.setHost(user); + + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule.builder() + .meetingId(meeting.getId()) + .userId(user.getId()) + .availableDate(LocalDate.of(2024, 7, 9)) + .weight(0) + .startTimeSlot(TimeSlot.SLOT_11_00) + .endTimeSlot(TimeSlot.SLOT_16_30) + .build(); + + em.persist(userMeetingSchedule); + + String encodedMeetingId = secureUrlUtil.encodeUrl(meeting.getId()); + String hostJwtToken = jwtService.issuedToken(String.valueOf(user.getId())); + + String body = objectMapper.writeValueAsString(availableTimes); + + // when, then + mockMvc.perform( + post("/user/host/" + encodedMeetingId + "/time") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header("Authorization", "Bearer " + hostJwtToken) + ).andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("이미 가능 시간 입력을 마쳤습니다.")); + } + } +} diff --git a/src/test/java/com/asap/server/presentation/controller/user/UserRegisterControllerTest.java b/src/test/java/com/asap/server/presentation/controller/user/UserRegisterControllerTest.java new file mode 100644 index 00000000..de313734 --- /dev/null +++ b/src/test/java/com/asap/server/presentation/controller/user/UserRegisterControllerTest.java @@ -0,0 +1,113 @@ +package com.asap.server.presentation.controller.user; + +import static com.asap.server.common.exception.Error.HOST_MEETING_TIME_NOT_PROVIDED; +import static com.asap.server.common.exception.Error.INVALID_HOST_ID_PASSWORD_EXCEPTION; +import static com.asap.server.common.exception.Error.MEETING_VALIDATION_FAILED_EXCEPTION; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.asap.server.common.exception.model.ConflictException; +import com.asap.server.common.exception.model.HostTimeForbiddenException; +import com.asap.server.common.exception.model.UnauthorizedException; +import com.asap.server.presentation.controller.dto.request.HostLoginRequestDto; +import com.asap.server.service.user.UserLoginService; +import com.fasterxml.jackson.databind.ObjectMapper; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + + +@SpringBootTest +@AutoConfigureMockMvc +class UserRegisterControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockBean + private UserLoginService userLoginService; + + @DisplayName("아직 확정되지 않은 특정 회의의 방장의 이름과 비밀번호가 일치하면 200 응답과 accessToken을 반환한다.") + @Test + void test() throws Exception { + // given + String encodedMeetingId = "MQ=="; + HostLoginRequestDto bodyDto = new HostLoginRequestDto("KWY", "0000"); + String body = objectMapper.writeValueAsString(bodyDto); + when(userLoginService.loginByHost(1L, "KWY", "0000")).thenReturn("access token"); + + mockMvc.perform( + post("/user/" + encodedMeetingId + "/host") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("로그인 성공입니다")) + .andExpect(jsonPath("$.data.accessToken").value("access token")); + } + + @DisplayName("방장 이름 또는 특정 회의의 비밀번호가 일치하지 않는다면 401 에러를 반환한다.") + @Test + void test2() throws Exception { + // given + String encodedMeetingId = "MQ=="; + HostLoginRequestDto bodyDto = new HostLoginRequestDto("USER1", "0000"); + String body = objectMapper.writeValueAsString(bodyDto); + when(userLoginService.loginByHost(1L, "USER1", "0000")) + .thenThrow(new UnauthorizedException(INVALID_HOST_ID_PASSWORD_EXCEPTION)); + + mockMvc.perform( + post("/user/" + encodedMeetingId + "/host") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.message").value("유효하지 않은 사용자 이름 또는 비밀번호입니다.")); + } + + @DisplayName("이미 확정된 회의라면 409 에러를 반환한다.") + @Test + void test3() throws Exception { + // given + String encodedMeetingId = "MQ=="; + HostLoginRequestDto bodyDto = new HostLoginRequestDto("KWY", "0000"); + String body = objectMapper.writeValueAsString(bodyDto); + when(userLoginService.loginByHost(1L, "KWY", "0000")) + .thenThrow(new ConflictException(MEETING_VALIDATION_FAILED_EXCEPTION)); + + mockMvc.perform( + post("/user/" + encodedMeetingId + "/host") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("이미 확정된 회의입니다.")); + } + + @DisplayName("방장이 시간을 입력하지 않았다면 403 에러와 함께 jwt 토큰을 반환한다.") + @Test + void test4() throws Exception { + // given + String encodedMeetingId = "MQ=="; + HostLoginRequestDto bodyDto = new HostLoginRequestDto("KWY", "0000"); + String body = objectMapper.writeValueAsString(bodyDto); + when(userLoginService.loginByHost(1L, "KWY", "0000")) + .thenThrow(new HostTimeForbiddenException(HOST_MEETING_TIME_NOT_PROVIDED, "access token")); + + mockMvc.perform( + post("/user/" + encodedMeetingId + "/host") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").value("회의 가능 시간이 입력되지 않았습니다.")) + .andExpect(jsonPath("$.data.accessToken").value("access token")); + } +} \ No newline at end of file diff --git a/src/test/java/com/asap/server/service/internal/MetricsServiceTest.java b/src/test/java/com/asap/server/service/internal/MetricsServiceTest.java new file mode 100644 index 00000000..06dae094 --- /dev/null +++ b/src/test/java/com/asap/server/service/internal/MetricsServiceTest.java @@ -0,0 +1,146 @@ +package com.asap.server.service.internal; + + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.asap.server.common.exception.model.BadRequestException; +import com.asap.server.infra.slack.MetricsEvent; +import com.asap.server.persistence.repository.internal.MetricsRepository; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@ExtendWith(MockitoExtension.class) +class MetricsServiceTest { + @Mock + private MetricsRepository metricsRepository; + @Mock + private ApplicationEventPublisher publisher; + @InjectMocks + private MetricsService metricsService; + + @DisplayName("날짜 형식은 yyyy-MM-dd 형식으로 입력한다.") + @Test + void test() { + // given + String fromStr = "2024-08-24"; + String toStr = "2024-08-26"; + + LocalDateTime from = LocalDate.parse(fromStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(); + LocalDateTime to = LocalDate.parse(toStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(); + + when(metricsRepository.countTotalMeetingCount(from, to)).thenReturn(1L); + when(metricsRepository.countTotalUserCount(from, to)).thenReturn(1L); + when(metricsRepository.countTotalConfirmedMeetingCount(from, to)).thenReturn(1L); + Map metrics = Map.of( + "개설된 총 회의 수", "1", + "사용한 총 사용자 수", "1", + "확정된 총 회의 수", "1" + ); + + // when + metricsService.sendMetrics(fromStr, toStr); + + // then + verify(publisher, times(1)).publishEvent(new MetricsEvent(metrics)); + } + + @DisplayName("날짜 형식(yyyy-MM-dd)과 다른 형식으로 입력했을 때, BadRequestException을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"2024/08/24", "2024 08 24", "2024-13-24", "2024/08/32", "2024-13-30"}) + void test2(String fromStr) { + // given + String toStr = "2024-08-26"; + + // when + assertThatThrownBy(() -> metricsService.sendMetrics(fromStr, toStr)) + .isInstanceOf(BadRequestException.class) + .hasMessage("유효하지 않은 날짜를 입력했습니다."); + } + + @DisplayName("시작, 종료 날짜 모두 들어오지 않을 수 있다.") + @Test + void test3() { + // given + String fromStr = null; + String toStr = null; + + when(metricsRepository.countTotalMeetingCount(null, null)).thenReturn(1L); + when(metricsRepository.countTotalUserCount(null, null)).thenReturn(1L); + when(metricsRepository.countTotalConfirmedMeetingCount(null, null)).thenReturn(1L); + Map metrics = Map.of( + "개설된 총 회의 수", "1", + "사용한 총 사용자 수", "1", + "확정된 총 회의 수", "1" + ); + + // when + metricsService.sendMetrics(fromStr, toStr); + + // then + verify(publisher, times(1)).publishEvent(new MetricsEvent(metrics)); + } + + @DisplayName("시작 날짜는 들어오지 않고, 종료 날짜만 들어올 수 있다.") + @Test + void test4() { + // given + String fromStr = null; + String toStr = "2024-08-26"; + + LocalDateTime to = LocalDate.parse(toStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(); + + when(metricsRepository.countTotalMeetingCount(null, to)).thenReturn(1L); + when(metricsRepository.countTotalUserCount(null, to)).thenReturn(1L); + when(metricsRepository.countTotalConfirmedMeetingCount(null, to)).thenReturn(1L); + Map metrics = Map.of( + "개설된 총 회의 수", "1", + "사용한 총 사용자 수", "1", + "확정된 총 회의 수", "1" + ); + + // when + metricsService.sendMetrics(fromStr, toStr); + + // then + verify(publisher, times(1)).publishEvent(new MetricsEvent(metrics)); + } + + @DisplayName("시작 날짜는 들어오고, 종료 날짜는 들어오지 않을 수 있다.") + @Test + void test5() { + // given + String fromStr = "2024-08-26"; + String toStr = null; + + LocalDateTime from = LocalDate.parse(fromStr, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(); + + when(metricsRepository.countTotalMeetingCount(from, null)).thenReturn(1L); + when(metricsRepository.countTotalUserCount(from, null)).thenReturn(1L); + when(metricsRepository.countTotalConfirmedMeetingCount(from, null)).thenReturn(1L); + Map metrics = Map.of( + "개설된 총 회의 수", "1", + "사용한 총 사용자 수", "1", + "확정된 총 회의 수", "1" + ); + + // when + metricsService.sendMetrics(fromStr, toStr); + + // then + verify(publisher, times(1)).publishEvent(new MetricsEvent(metrics)); + } +} \ No newline at end of file diff --git a/src/test/java/com/asap/server/service/meeting/ConfirmMeetingMethodTest.java b/src/test/java/com/asap/server/service/meeting/ConfirmMeetingMethodTest.java index f7373b00..516ee360 100644 --- a/src/test/java/com/asap/server/service/meeting/ConfirmMeetingMethodTest.java +++ b/src/test/java/com/asap/server/service/meeting/ConfirmMeetingMethodTest.java @@ -1,10 +1,11 @@ package com.asap.server.service.meeting; +import com.asap.server.persistence.domain.user.Name; import com.asap.server.presentation.controller.dto.request.MeetingConfirmRequestDto; import com.asap.server.presentation.controller.dto.request.UserRequestDto; import com.asap.server.persistence.domain.Meeting; import com.asap.server.persistence.domain.Place; -import com.asap.server.persistence.domain.User; +import com.asap.server.persistence.domain.user.User; import com.asap.server.persistence.domain.enums.Duration; import com.asap.server.persistence.domain.enums.PlaceType; import com.asap.server.persistence.domain.enums.Role; @@ -43,9 +44,11 @@ void setConfirmDateTimeTest() { .duration(Duration.HALF) .place(place) .build(); + + final Name name = new Name("KWY"); final User user = User.builder() .meeting(meeting) - .name("강원용") + .name(name) .role(Role.HOST) .isFixed(false) .build(); diff --git a/src/test/java/com/asap/server/service/meeting/MeetingRetrieveServiceTest.java b/src/test/java/com/asap/server/service/meeting/MeetingRetrieveServiceTest.java new file mode 100644 index 00000000..64007826 --- /dev/null +++ b/src/test/java/com/asap/server/service/meeting/MeetingRetrieveServiceTest.java @@ -0,0 +1,332 @@ +package com.asap.server.service.meeting; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +import com.asap.server.common.utils.DateUtil; +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.enums.Duration; +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; +import com.asap.server.persistence.repository.meeting.MeetingRepository; +import com.asap.server.service.meeting.dto.BestMeetingTimeDto; +import com.asap.server.service.meeting.dto.UserDto; +import com.asap.server.service.time.MeetingTimeRecommendService; +import com.asap.server.service.time.UserMeetingScheduleService; +import com.asap.server.service.time.dto.retrieve.AvailableDatesRetrieveDto; +import com.asap.server.service.time.dto.retrieve.TimeBlockRetrieveDto; +import com.asap.server.service.time.dto.retrieve.TimeTableRetrieveDto; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.user.UserRetrieveService; +import com.asap.server.service.time.vo.BestMeetingTimeVo; +import com.asap.server.service.time.vo.BestMeetingTimeWithUsers; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MeetingRetrieveServiceTest { + @Mock + private MeetingRepository meetingRepository; + @Mock + private UserRetrieveService userRetrieveService; + @Mock + private MeetingTimeRecommendService meetingTimeRecommendService; + @Mock + private UserMeetingScheduleService userMeetingScheduleService; + @InjectMocks + private MeetingRetrieveService meetingRetrieveService; + + @Nested + @DisplayName("정상 응답 테스트") + class SuccessTestCase { + @DisplayName("추천된 회의 시간이 1가지 일 때, (회의 시간, null, null)을 반환한다.") + @Test + void test() { + // given + Meeting meeting = Meeting.builder() + .id(1L) + .host(User.builder().id(1L).build()) + .duration(Duration.HALF) + .build(); + User user = User.builder() + .id(1L) + .name(new Name("KWY")) + .build(); + User user2 = User.builder() + .id(2L) + .name(new Name("DSH")) + .build(); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)) + ); + when(meetingRepository.findById(1L)).thenReturn(Optional.of(meeting)); + when(userRetrieveService.getMeetingUserCount(meeting)).thenReturn(2); + when(userMeetingScheduleService.getTimeBlocks(1L)).thenReturn(timeBlocks); + when(meetingTimeRecommendService.getBestMeetingTime(timeBlocks, meeting.getDuration(), 2)).thenReturn( + Arrays.asList( + new BestMeetingTimeVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_12_00, TimeSlot.SLOT_12_30, 0, + List.of(1L, 2L)), + null, + null + ) + ); + when(userRetrieveService.getUserIdToUserMap(1L)).thenReturn(Map.of(1L, user, 2L, user2)); + + List bestMeetingTimeWithUsers = Arrays.asList( + new BestMeetingTimeWithUsers( + LocalDate.of(2024, 7, 9), + TimeSlot.SLOT_12_00, + TimeSlot.SLOT_12_30, + 0, + List.of( + new UserDto(1L, "KWY"), + new UserDto(2L, "DSH") + ) + ), + null, + null + ); + BestMeetingTimeDto expected = BestMeetingTimeDto.of(2, bestMeetingTimeWithUsers); + + // when + BestMeetingTimeDto result = meetingRetrieveService.getBestMeetingTime(1L, 1L); + + // then + assertThat(expected).isEqualTo(result); + } + + @DisplayName("추천된 회의 시간이 2가지 일 때, (회의 시간1, 회의 시간2, null)을 반환한다.") + @Test + void test2() { + // given + Meeting meeting = Meeting.builder() + .id(1L) + .host(User.builder().id(1L).build()) + .duration(Duration.HALF) + .build(); + User user = User.builder() + .id(1L) + .name(new Name("KWY")) + .build(); + User user2 = User.builder() + .id(2L) + .name(new Name("DSH")) + .build(); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)) + ); + when(meetingRepository.findById(1L)).thenReturn(Optional.of(meeting)); + when(userRetrieveService.getMeetingUserCount(meeting)).thenReturn(2); + when(userMeetingScheduleService.getTimeBlocks(1L)).thenReturn(timeBlocks); + when(meetingTimeRecommendService.getBestMeetingTime(timeBlocks, meeting.getDuration(), 2)).thenReturn( + Arrays.asList( + new BestMeetingTimeVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_12_00, TimeSlot.SLOT_12_30, 0, + List.of(1L, 2L)), + new BestMeetingTimeVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_13_00, TimeSlot.SLOT_13_30, 0, + List.of(1L, 2L)), + null + ) + ); + when(userRetrieveService.getUserIdToUserMap(1L)).thenReturn(Map.of(1L, user, 2L, user2)); + + List bestMeetingTimeWithUsers = Arrays.asList( + new BestMeetingTimeWithUsers( + LocalDate.of(2024, 7, 9), + TimeSlot.SLOT_12_00, + TimeSlot.SLOT_12_30, + 0, + List.of( + new UserDto(1L, "KWY"), + new UserDto(2L, "DSH") + ) + ), + new BestMeetingTimeWithUsers( + LocalDate.of(2024, 7, 9), + TimeSlot.SLOT_13_00, + TimeSlot.SLOT_13_30, + 0, + List.of( + new UserDto(1L, "KWY"), + new UserDto(2L, "DSH") + ) + ), + null + ); + BestMeetingTimeDto expected = BestMeetingTimeDto.of(2, bestMeetingTimeWithUsers); + + // when + BestMeetingTimeDto result = meetingRetrieveService.getBestMeetingTime(1L, 1L); + + // then + assertThat(expected).isEqualTo(result); + } + + @DisplayName("추천된 회의 시간이 3가지 일 때, (회의 시간1, 회의 시간2, 회의 시간3)을 반환한다.") + @Test + void test3() { + // given + Meeting meeting = Meeting.builder() + .id(1L) + .host(User.builder().id(1L).build()) + .duration(Duration.HALF) + .build(); + User user = User.builder() + .id(1L) + .name(new Name("KWY")) + .build(); + User user2 = User.builder() + .id(2L) + .name(new Name("DSH")) + .build(); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_00, 0, List.of(1L, 2L)) + ); + when(meetingRepository.findById(1L)).thenReturn(Optional.of(meeting)); + when(userRetrieveService.getMeetingUserCount(meeting)).thenReturn(2); + when(userMeetingScheduleService.getTimeBlocks(1L)).thenReturn(timeBlocks); + when(meetingTimeRecommendService.getBestMeetingTime(timeBlocks, meeting.getDuration(), 2)).thenReturn( + Arrays.asList( + new BestMeetingTimeVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_12_00, TimeSlot.SLOT_12_30, 0, + List.of(1L, 2L)), + new BestMeetingTimeVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_13_00, TimeSlot.SLOT_13_30, 0, + List.of(1L, 2L)), + new BestMeetingTimeVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_14_00, TimeSlot.SLOT_14_30, 0, + List.of(1L, 2L)) + ) + ); + when(userRetrieveService.getUserIdToUserMap(1L)).thenReturn(Map.of(1L, user, 2L, user2)); + + List bestMeetingTimeWithUsers = Arrays.asList( + new BestMeetingTimeWithUsers( + LocalDate.of(2024, 7, 9), + TimeSlot.SLOT_12_00, + TimeSlot.SLOT_12_30, + 0, + List.of( + new UserDto(1L, "KWY"), + new UserDto(2L, "DSH") + ) + ), + new BestMeetingTimeWithUsers( + LocalDate.of(2024, 7, 9), + TimeSlot.SLOT_13_00, + TimeSlot.SLOT_13_30, + 0, + List.of( + new UserDto(1L, "KWY"), + new UserDto(2L, "DSH") + ) + ), + new BestMeetingTimeWithUsers( + LocalDate.of(2024, 7, 9), + TimeSlot.SLOT_14_00, + TimeSlot.SLOT_14_30, + 0, + List.of( + new UserDto(1L, "KWY"), + new UserDto(2L, "DSH") + ) + ) + ); + BestMeetingTimeDto expected = BestMeetingTimeDto.of(2, bestMeetingTimeWithUsers); + + // when + BestMeetingTimeDto result = meetingRetrieveService.getBestMeetingTime(1L, 1L); + + // then + assertThat(expected).isEqualTo(result); + } + } + + @Nested + @DisplayName("회의 종합 일정 시간표 테스트") + class MeetingTimeTableTest { + + @Test + @DisplayName("각 참여자 수에 맞는 colorLevel을 반환한다.") + void testTimeTable() { + + // given + Meeting meeting = Meeting.builder() + .id(1L) + .host(User.builder().id(1L).build()) + .duration(Duration.HALF) + .build(); + User user = User.builder() + .id(1L) + .name(new Name("KWY")) + .build(); + User user2 = User.builder() + .id(2L) + .name(new Name("DSH")) + .build(); + User user3 = User.builder() + .id(3L) + .name(new Name("SJW")) + .build(); + User user4 = User.builder() + .id(4L) + .name(new Name("SCW")) + .build(); + User user5 = User.builder() + .id(5L) + .name(new Name("KTH")) + .build(); + LocalDate date = LocalDate.of(2024, 7, 9); + List timeBlocks = List.of( + new TimeBlockVo(date, TimeSlot.SLOT_12_00, 0, List.of(1L, 2L, 3L, 4L, 5L)), + new TimeBlockVo(date, TimeSlot.SLOT_13_00, 0, List.of(1L, 2L, 3L, 4L)), + new TimeBlockVo(date, TimeSlot.SLOT_14_00, 0, List.of(1L, 2L, 3L)), + new TimeBlockVo(date, TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)), + new TimeBlockVo(date, TimeSlot.SLOT_16_00, 0, List.of(1L)) + ); + + when(meetingRepository.findById(1L)).thenReturn(Optional.of(meeting)); + when(userMeetingScheduleService.getTimeBlocks(1L)).thenReturn(timeBlocks); + when(userRetrieveService.getUserIdToUserMap(1L)).thenReturn(Map.of(1L, user, 2L, user2, 3L, user3, 4L, user4, 5L, user5)); + + + List expectedTimeSlotDto = List.of( + new TimeBlockRetrieveDto("12:00", List.of("KWY", "DSH", "SJW", "SCW", "KTH"), 5), + new TimeBlockRetrieveDto("13:00", List.of("KWY", "DSH", "SJW", "SCW"), 4), + new TimeBlockRetrieveDto("14:00", List.of("KWY", "DSH", "SJW"), 3), + new TimeBlockRetrieveDto("15:00", List.of("KWY", "DSH"), 2), + new TimeBlockRetrieveDto("16:00", List.of("KWY"), 1) + ); + + List expectedAvailableDto = List.of( + new AvailableDatesRetrieveDto( + DateUtil.getMonth(date), + DateUtil.getDay(date), + DateUtil.getDayOfWeek(date), + expectedTimeSlotDto + ) + ); + TimeTableRetrieveDto expected = TimeTableRetrieveDto.of( + List.of("KWY", "DSH", "SJW", "SCW", "KTH"), + expectedAvailableDto + ); + + TimeTableRetrieveDto result = meetingRetrieveService.getTimeTable(1L, 1L); + + assertThat(result).isEqualTo(expected); + } + + } +} \ No newline at end of file diff --git a/src/test/java/com/asap/server/service/meeting/recommend/MeetingTimeRecommendServiceTest.java b/src/test/java/com/asap/server/service/time/MeetingTimeRecommendServiceTest.java similarity index 51% rename from src/test/java/com/asap/server/service/meeting/recommend/MeetingTimeRecommendServiceTest.java rename to src/test/java/com/asap/server/service/time/MeetingTimeRecommendServiceTest.java index 3558b274..dd510059 100644 --- a/src/test/java/com/asap/server/service/meeting/recommend/MeetingTimeRecommendServiceTest.java +++ b/src/test/java/com/asap/server/service/time/MeetingTimeRecommendServiceTest.java @@ -1,23 +1,22 @@ -package com.asap.server.service.meeting.recommend; +package com.asap.server.service.time; import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_12_00; import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_12_30; import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_13_00; import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_13_30; import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_15_00; -import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_15_30; import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_16_00; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.asap.server.common.generator.TimeBlockDtoGenerator; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import com.asap.server.service.meeting.recommend.strategy.impl.BestMeetingTimeStrategyImpl; -import com.asap.server.service.meeting.recommend.strategy.impl.ContinuousMeetingTimeStrategyImpl; -import com.asap.server.service.meeting.recommend.strategy.impl.MeetingTimeCasesStrategyImpl; -import com.asap.server.service.vo.BestMeetingTimeVo; +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.service.time.MeetingTimeRecommendService; +import com.asap.server.service.time.strategy.impl.BestMeetingTimeStrategyImpl; +import com.asap.server.service.time.strategy.impl.ContinuousMeetingTimeStrategyImpl; +import com.asap.server.service.time.strategy.impl.MeetingTimeCasesStrategyImpl; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.time.vo.BestMeetingTimeVo; import java.time.LocalDate; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -41,10 +40,10 @@ public void setUp() { public void getBestMeetingTime() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); - TimeBlockDto timeBlock = new TimeBlockDto(availableDate, SLOT_12_00, 0, 1L); - List timeBlocks = List.of(timeBlock); + TimeBlockVo timeBlock = new TimeBlockVo(availableDate, SLOT_12_00, 0, List.of(1L)); + List timeBlocks = List.of(timeBlock); - BestMeetingTimeVo expected = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0); + BestMeetingTimeVo expected = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0, List.of(1L)); // when List result = meetingTimeRecommendService.getBestMeetingTime(timeBlocks, Duration.HALF, 1); @@ -60,12 +59,12 @@ public void getBestMeetingTime2() { LocalDate availableDate = LocalDate.of(2023, 7, 10); LocalDate availableDate2 = LocalDate.of(2023, 7, 11); - TimeBlockDto timeBlock = new TimeBlockDto(availableDate, SLOT_12_00, 0, 1L); - TimeBlockDto timeBlock2 = new TimeBlockDto(availableDate2, SLOT_12_30, 0, 1L); - List timeBlocks = List.of(timeBlock, timeBlock2); + TimeBlockVo timeBlock = new TimeBlockVo(availableDate, SLOT_12_00, 0, List.of(1L)); + TimeBlockVo timeBlock2 = new TimeBlockVo(availableDate2, SLOT_12_30, 0, List.of(1L)); + List timeBlocks = List.of(timeBlock, timeBlock2); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0, List.of(1L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_00, 0, List.of(1L)); List expected = Arrays.asList(e1, e2, null); // when @@ -83,15 +82,15 @@ public void getBestMeetingTime3() { LocalDate availableDate = LocalDate.of(2023, 7, 10); LocalDate availableDate2 = LocalDate.of(2023, 7, 11); - TimeBlockDto timeBlock = new TimeBlockDto(availableDate, SLOT_12_00, 0, 2L); - TimeBlockDto timeBlock2 = new TimeBlockDto(availableDate, SLOT_13_00, 0, 2L); - TimeBlockDto timeBlock3 = new TimeBlockDto(availableDate2, SLOT_12_30, 0, 2L); - TimeBlockDto timeBlock4 = new TimeBlockDto(availableDate2, SLOT_13_00, 0, 2L); - List timeBlocks = List.of(timeBlock, timeBlock2, timeBlock3, timeBlock4); + TimeBlockVo timeBlock = new TimeBlockVo(availableDate, SLOT_12_00, 0, List.of(1L, 2L)); + TimeBlockVo timeBlock2 = new TimeBlockVo(availableDate, SLOT_13_00, 0, List.of(1L, 2L)); + TimeBlockVo timeBlock3 = new TimeBlockVo(availableDate2, SLOT_12_30, 0, List.of(1L, 2L)); + TimeBlockVo timeBlock4 = new TimeBlockVo(availableDate2, SLOT_13_00, 0, List.of(1L, 2L)); + List timeBlocks = List.of(timeBlock, timeBlock2, timeBlock3, timeBlock4); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, SLOT_13_00, SLOT_13_30, 0); - BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, SLOT_13_00, SLOT_13_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2, e3); // when @@ -106,21 +105,24 @@ public void getBestMeetingTime3() { @DisplayName("최적의 회의시간이 2개이고 차선의 경우가 1개 있는 경우") public void getBestMeetingTime4() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - LocalDate availableDate2 = LocalDate.of(2023, 7, 11); + LocalDate availableDate = LocalDate.of(2024, 7, 10); + LocalDate availableDate2 = LocalDate.of(2024, 7, 11); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)) + ); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_15_30, 0, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate2, SLOT_12_30, SLOT_13_00, 0, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - }}; - - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_00, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, SLOT_15_00, SLOT_16_00, 0); - BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, SLOT_15_00, SLOT_16_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_30, 0, List.of(1L, 2L)); List expected = List.of(e1, e2, e3); // when @@ -135,21 +137,25 @@ public void getBestMeetingTime4() { @DisplayName("최적의 회의 시간이 3개일 때 우선순위가 가장 높은 회의 시간이 첫번째에 위치한다") public void getBestMeetingTime5() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - LocalDate availableDate2 = LocalDate.of(2023, 7, 11); + LocalDate availableDate = LocalDate.of(2024, 7, 10); + LocalDate availableDate2 = LocalDate.of(2024, 7, 11); + + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_12_30, 6, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_13_00, 6, List.of(1L, 2L)) + ); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_15_30, 0, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate2, SLOT_12_30, SLOT_13_00, 6, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - }}; - - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_30, 6); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_00, 0); - BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate, SLOT_15_00, SLOT_16_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate2, SLOT_12_30, SLOT_13_30, 6, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate, SLOT_15_00, SLOT_16_00, 0, List.of(1L, 2L)); List expected = Arrays.asList(e1, e2, e3); // when @@ -166,10 +172,10 @@ public void getBestMeetingTime6() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); - TimeBlockDto timeBlock = new TimeBlockDto(availableDate, SLOT_12_00, 0, 2L); - List timeBlocks = List.of(timeBlock); + TimeBlockVo timeBlock = new TimeBlockVo(availableDate, SLOT_12_00, 0, List.of(1L, 2L)); + List timeBlocks = List.of(timeBlock); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0, List.of(1L, 2L)); List expected = Arrays.asList(e1, null, null); // when @@ -187,16 +193,16 @@ public void getBestMeetingTime7() { LocalDate availableDate2 = LocalDate.of(2023, 7, 11); LocalDate availableDate3 = LocalDate.of(2023, 7, 12); - TimeBlockDto timeBlock = new TimeBlockDto(availableDate, SLOT_12_00, 3, 3L); - TimeBlockDto timeBlock2 = new TimeBlockDto(availableDate, SLOT_12_30, 3, 3L); - TimeBlockDto timeBlock3 = new TimeBlockDto(availableDate2, SLOT_12_00, 4, 3L); - TimeBlockDto timeBlock4 = new TimeBlockDto(availableDate3, SLOT_12_00, 4, 3L); + TimeBlockVo timeBlock = new TimeBlockVo(availableDate, SLOT_12_00, 3, List.of(1L, 2L, 3L)); + TimeBlockVo timeBlock2 = new TimeBlockVo(availableDate, SLOT_12_30, 3, List.of(1L, 2L, 3L)); + TimeBlockVo timeBlock3 = new TimeBlockVo(availableDate2, SLOT_12_00, 4, List.of(1L, 2L, 3L)); + TimeBlockVo timeBlock4 = new TimeBlockVo(availableDate3, SLOT_12_00, 4, List.of(1L, 2L, 3L)); - List timeBlocks = List.of(timeBlock, timeBlock2, timeBlock3, timeBlock4); + List timeBlocks = List.of(timeBlock, timeBlock2, timeBlock3, timeBlock4); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_00, 3); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate2, SLOT_12_00, SLOT_12_30, 4); - BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate3, SLOT_12_00, SLOT_12_30, 4); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_00, 3, List.of(1L, 2L, 3L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate2, SLOT_12_00, SLOT_12_30, 4, List.of(1L, 2L, 3L)); + BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate3, SLOT_12_00, SLOT_12_30, 4, List.of(1L, 2L, 3L)); List expected = Arrays.asList(e1, e2, e3); // when @@ -210,12 +216,18 @@ public void getBestMeetingTime7() { @DisplayName("이미 추천한 시간대는 이후 조합에서 추천하지 않는다.") public void getBestMeetingTime8() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - - List timeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_15_00, 0, 2L); + LocalDate availableDate = LocalDate.of(2024, 7, 10); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)) + ); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_13_30, 0, List.of(1L, 2L)); List expected = Arrays.asList(e1, null, null); // when diff --git a/src/test/java/com/asap/server/service/time/UserMeetingScheduleServiceTest.java b/src/test/java/com/asap/server/service/time/UserMeetingScheduleServiceTest.java new file mode 100644 index 00000000..d011090c --- /dev/null +++ b/src/test/java/com/asap/server/service/time/UserMeetingScheduleServiceTest.java @@ -0,0 +1,223 @@ +package com.asap.server.service.time; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.persistence.domain.time.UserMeetingSchedule; +import com.asap.server.persistence.repository.UserMeetingScheduleRepository; +import com.asap.server.service.time.vo.TimeBlockVo; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserMeetingScheduleServiceTest { + @Mock + private UserMeetingScheduleRepository userMeetingScheduleRepository; + @InjectMocks + private UserMeetingScheduleService userMeetingScheduleService; + + @Test + @DisplayName("input 6:00 - 08:00 , return 06:00 - 07:30") + void test() { + // given + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule + .builder() + .userId(1L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_6_00) + .endTimeSlot(TimeSlot.SLOT_8_00) + .build(); + + UserMeetingSchedule userMeetingSchedule2 = UserMeetingSchedule + .builder() + .userId(2L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_6_00) + .endTimeSlot(TimeSlot.SLOT_8_00) + .build(); + + List userMeetingSchedules = List.of(userMeetingSchedule, userMeetingSchedule2); + when(userMeetingScheduleRepository.findAllByMeetingId(1L)).thenReturn(userMeetingSchedules); + + List expected = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_6_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_6_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_7_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_7_30, 0, List.of(1L, 2L)) + ); + + // when + List response = userMeetingScheduleService.getTimeBlocks(1L); + + // then + assertThat(response).isEqualTo(expected); + } + + @Test + @DisplayName("input empty list , return empty list") + void test2() { + // given + when(userMeetingScheduleRepository.findAllByMeetingId(1L)).thenReturn(Collections.emptyList()); + + // when + List response = userMeetingScheduleService.getTimeBlocks(1L); + + // then + assertThat(response.isEmpty()).isTrue(); + } + + @Test + @DisplayName("input [06:00 - 07:00, 가중치: 1], [06:00 - 08:00, 가중치: 2], return [06:00 - 06:30 가중치 3], [07:00 - 07:30 가중치 2]") + void test3() { + // given + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule + .builder() + .userId(1L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_6_00) + .endTimeSlot(TimeSlot.SLOT_7_00) + .weight(1) + .build(); + + UserMeetingSchedule userMeetingSchedule2 = UserMeetingSchedule + .builder() + .userId(2L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_6_00) + .endTimeSlot(TimeSlot.SLOT_8_00) + .weight(2) + .build(); + + List userMeetingSchedules = List.of(userMeetingSchedule, userMeetingSchedule2); + when(userMeetingScheduleRepository.findAllByMeetingId(1L)).thenReturn(userMeetingSchedules); + + List expected = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_6_00, 3, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_6_30, 3, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_7_00, 2, List.of(2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_7_30, 2, List.of(2L)) + ); + + // when + List response = userMeetingScheduleService.getTimeBlocks(1L); + + // then + assertThat(response).isEqualTo(expected); + } + + @Test + @DisplayName("input [유저 1: 10:00 - 13:30], [유저 2: 08:30 - 11:00, 11:00 - 13:00], return [08:30 - 09:30 유저 2], [10:00 - 12:30 유저 1,2], [13:00, 유저 1]") + void test4() { + // given + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule + .builder() + .userId(1L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_10_00) + .endTimeSlot(TimeSlot.SLOT_13_30) + .weight(0) + .build(); + + UserMeetingSchedule userMeetingSchedule2 = UserMeetingSchedule + .builder() + .userId(2L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_8_30) + .endTimeSlot(TimeSlot.SLOT_11_00) + .weight(0) + .build(); + UserMeetingSchedule userMeetingSchedule3 = UserMeetingSchedule + .builder() + .userId(2L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_11_00) + .endTimeSlot(TimeSlot.SLOT_13_00) + .weight(0) + .build(); + + List userMeetingSchedules = + List.of(userMeetingSchedule, userMeetingSchedule2, userMeetingSchedule3); + when(userMeetingScheduleRepository.findAllByMeetingId(1L)).thenReturn(userMeetingSchedules); + + List expected = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_8_30, 0, List.of(2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_9_00, 0, List.of(2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_9_30, 0, List.of(2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_10_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_10_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_11_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_11_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_13_00, 0, List.of(1L)) + ); + + // when + List response = userMeetingScheduleService.getTimeBlocks(1L); + + // then + assertThat(response).isEqualTo(expected); + } + + @Test + @DisplayName("input [유저 1: 6:00 - 6:30] return [06:00, 유저 1]") + void test5() { + // given + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule + .builder() + .userId(1L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_6_00) + .endTimeSlot(TimeSlot.SLOT_6_30) + .weight(0) + .build(); + + List userMeetingSchedules = List.of(userMeetingSchedule); + when(userMeetingScheduleRepository.findAllByMeetingId(1L)).thenReturn(userMeetingSchedules); + + List expected = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_6_00, 0, List.of(1L)) + ); + + // when + List response = userMeetingScheduleService.getTimeBlocks(1L); + + // then + assertThat(response).isEqualTo(expected); + } + + @Test + @DisplayName("input [유저 1: 23:30 - 24:00] return [23:30, 유저 1]") + void test6() { + // given + UserMeetingSchedule userMeetingSchedule = UserMeetingSchedule + .builder() + .userId(1L) + .availableDate(LocalDate.of(2024, 7, 9)) + .startTimeSlot(TimeSlot.SLOT_23_30) + .endTimeSlot(TimeSlot.SLOT_24_00) + .weight(0) + .build(); + + List userMeetingSchedules = List.of(userMeetingSchedule); + when(userMeetingScheduleRepository.findAllByMeetingId(1L)).thenReturn(userMeetingSchedules); + + List expected = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 9), TimeSlot.SLOT_23_30, 0, List.of(1L)) + ); + + // when + List response = userMeetingScheduleService.getTimeBlocks(1L); + + // then + assertThat(response).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/BestMeetingTimeStrategyImplTest.java b/src/test/java/com/asap/server/service/time/strategy/impl/BestMeetingTimeStrategyImplTest.java similarity index 83% rename from src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/BestMeetingTimeStrategyImplTest.java rename to src/test/java/com/asap/server/service/time/strategy/impl/BestMeetingTimeStrategyImplTest.java index f5364b8c..b878072d 100644 --- a/src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/BestMeetingTimeStrategyImplTest.java +++ b/src/test/java/com/asap/server/service/time/strategy/impl/BestMeetingTimeStrategyImplTest.java @@ -1,11 +1,11 @@ -package com.asap.server.service.meeting.recommend.strategy.impl; +package com.asap.server.service.time.strategy.impl; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import com.asap.server.persistence.domain.enums.Duration; import com.asap.server.persistence.domain.enums.TimeSlot; -import com.asap.server.service.meeting.recommend.strategy.BestMeetingTimeStrategy; -import com.asap.server.service.vo.BestMeetingTimeVo; +import com.asap.server.service.time.strategy.BestMeetingTimeStrategy; +import com.asap.server.service.time.vo.BestMeetingTimeVo; import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -26,10 +26,10 @@ void test() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_14_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -45,11 +45,11 @@ void test2() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_14_30, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_16_30, TimeSlot.SLOT_17_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_16_30, TimeSlot.SLOT_17_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2); // when @@ -65,14 +65,14 @@ void test3() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_6_00, TimeSlot.SLOT_6_30, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_6_00, TimeSlot.SLOT_6_30, 0, List.of(1L, 2L)); BestMeetingTimeVo bestMeetingTimeVo2 = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_21_00, TimeSlot.SLOT_24_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_21_00, TimeSlot.SLOT_24_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo, bestMeetingTimeVo2); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_6_00, TimeSlot.SLOT_6_30, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_21_00, TimeSlot.SLOT_21_30, 0); - BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_23_30, TimeSlot.SLOT_24_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_6_00, TimeSlot.SLOT_6_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_21_00, TimeSlot.SLOT_21_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e3 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_23_30, TimeSlot.SLOT_24_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2, e3); // when @@ -94,10 +94,10 @@ void test() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -113,11 +113,11 @@ void test2() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_18_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_18_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_00, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_17_00, TimeSlot.SLOT_18_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_17_00, TimeSlot.SLOT_18_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2); // when @@ -139,10 +139,10 @@ void test() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_30, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_30, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -158,11 +158,11 @@ void test2() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_19_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_19_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_30, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_17_30, TimeSlot.SLOT_19_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_17_30, TimeSlot.SLOT_19_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2); // when @@ -184,10 +184,10 @@ void test() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -204,11 +204,11 @@ void test2() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_20_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_20_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_18_00, TimeSlot.SLOT_20_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_18_00, TimeSlot.SLOT_20_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2); // when @@ -230,10 +230,10 @@ void test() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_30, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -249,11 +249,11 @@ void test2() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_21_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_21_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_30, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_18_30, TimeSlot.SLOT_21_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_16_30, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_18_30, TimeSlot.SLOT_21_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2); // when @@ -275,10 +275,10 @@ void test() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -294,11 +294,11 @@ void test2() { // given LocalDate availableDate = LocalDate.of(2023, 7, 10); BestMeetingTimeVo bestMeetingTimeVo = - new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_22_00, 0); + new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_22_00, 0, List.of(1L, 2L)); List candidateMeetingTimes = List.of(bestMeetingTimeVo); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0); - BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_19_00, TimeSlot.SLOT_22_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_14_00, TimeSlot.SLOT_17_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo e2 = new BestMeetingTimeVo(availableDate, TimeSlot.SLOT_19_00, TimeSlot.SLOT_22_00, 0, List.of(1L, 2L)); List expected = List.of(e1, e2); // when diff --git a/src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/ContinuousMeetingTimeStrategyImplTest.java b/src/test/java/com/asap/server/service/time/strategy/impl/ContinuousMeetingTimeStrategyImplTest.java similarity index 51% rename from src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/ContinuousMeetingTimeStrategyImplTest.java rename to src/test/java/com/asap/server/service/time/strategy/impl/ContinuousMeetingTimeStrategyImplTest.java index 2c50a6e8..c6e457a8 100644 --- a/src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/ContinuousMeetingTimeStrategyImplTest.java +++ b/src/test/java/com/asap/server/service/time/strategy/impl/ContinuousMeetingTimeStrategyImplTest.java @@ -1,15 +1,27 @@ -package com.asap.server.service.meeting.recommend.strategy.impl; - -import static com.asap.server.persistence.domain.enums.TimeSlot.*; +package com.asap.server.service.time.strategy.impl; + +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_12_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_12_30; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_13_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_14_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_14_30; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_16_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_16_30; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_17_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_18_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_20_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_23_30; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_24_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_6_00; +import static com.asap.server.persistence.domain.enums.TimeSlot.SLOT_6_30; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.asap.server.common.generator.TimeBlockDtoGenerator; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.persistence.repository.timeblock.dto.TimeBlockDto; -import com.asap.server.service.meeting.recommend.strategy.ContinuousMeetingTimeStrategy; -import com.asap.server.service.vo.BestMeetingTimeVo; +import com.asap.server.persistence.domain.enums.TimeSlot; +import com.asap.server.service.time.strategy.ContinuousMeetingTimeStrategy; +import com.asap.server.service.time.vo.TimeBlockVo; +import com.asap.server.service.time.vo.BestMeetingTimeVo; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -24,11 +36,17 @@ class ContinuousMeetingTimeStrategyImplTest { @Nested class ReturnOneBestMeetingTimeVoByDuration { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - List timeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_15_30, 0, 2L); - - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_16_00, 0); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)) + ); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_16_00, 0, List.of(1L, 2L)); List result = List.of(r1); @DisplayName("회의 진행 시간이 주어졌을 때, 12시부터 16시까지인 BestMeetingTimeVo를 반환한다.") @@ -48,23 +66,28 @@ void durationsTest(Duration duration) { @Nested class ReturnBestMeetingTimeVoByDuration { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_15_30, 0, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate, SLOT_18_00, SLOT_19_30, 0, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - }}; + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_18_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_18_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_19_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_19_30, 0, List.of(1L, 2L)) + ); @DisplayName("회의 진행 시간이 2시간 이하일 때, 12시부터 16시까지, 18시부터 20시 까지인 BestMeetingTimeVo를 반환한다.") @ParameterizedTest @EnumSource(value = Duration.class, names = {"HALF", "HOUR", "HOUR_HALF", "TWO_HOUR"}) void durationsTest2(Duration duration) { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_16_00, 0); - BestMeetingTimeVo r2 = new BestMeetingTimeVo(availableDate, SLOT_18_00, SLOT_20_00, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_16_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo r2 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_18_00, SLOT_20_00, 0, List.of(1L, 2L)); List result = List.of(r1, r2); // when @@ -79,7 +102,7 @@ void durationsTest2(Duration duration) { @EnumSource(value = Duration.class, names = {"TWO_HOUR_HALF", "THREE_HOUR"}) void durationsTest3(Duration duration) { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_16_00, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_16_00, 0, List.of(1L, 2L)); List result = List.of(r1); // when @@ -94,14 +117,15 @@ void durationsTest3(Duration duration) { @Nested class ReturnOneBestMeetingTimeVoByDuration2 { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - List timeBlocks = TimeBlockDtoGenerator.generator(availableDate, SLOT_12_00, SLOT_12_00, 0, 2L); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)) + ); @DisplayName("회의 진행 시간이 30분일 때, 12시부터 12시 30분까지인 BestMeetingTimeVo를 반환한다.") @Test void durationHalfTest() { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_12_30, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_12_30, 0, List.of(1L, 2L)); List result = List.of(r1); // when @@ -126,24 +150,22 @@ void durationHourTest() { @Nested class ReturnBestMeetingTimeVoByDuration2 { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - LocalDate availableDate2 = LocalDate.of(2023, 7, 11); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_13_30, 0, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate2, SLOT_12_00, SLOT_12_30, 0, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - }}; + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)) + ); @DisplayName("회의 진행 시간이 1시간 이하일 때, 12시부터 14시, 12시부터 13시까지인 BestMeetingTimeVo를 반환한다.") @ParameterizedTest @EnumSource(value = Duration.class, names = {"HALF", "HOUR"}) void durationsTest(Duration duration) { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_14_00, 0); - BestMeetingTimeVo r2 = new BestMeetingTimeVo(availableDate2, SLOT_12_00, SLOT_13_00, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_14_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo r2 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 11), SLOT_12_00, SLOT_13_00, 0, List.of(1L, 2L)); List result = List.of(r1, r2); // when @@ -158,7 +180,7 @@ void durationsTest(Duration duration) { @EnumSource(value = Duration.class, names = {"HOUR_HALF", "TWO_HOUR"}) void durationsTest2(Duration duration) { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_14_00, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_14_00, 0, List.of(1L, 2L)); List result = List.of(r1); // when @@ -184,24 +206,25 @@ void durationsTest3(Duration duration) { @Nested class ReturnBestMeetingTimeVoByDuration3 { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - LocalDate availableDate2 = LocalDate.of(2023, 7, 11); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_13_30, 0, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate2, SLOT_14_30, SLOT_16_30, 0, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - }}; + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_14_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_15_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_15_30, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_16_00, 0, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_16_30, 0, List.of(1L, 2L)) + ); @DisplayName("회의 진행 시간이 2시간 이하일 때, 2023-7-10일 12시부터 14시, 2023-7-11일 14시 30분부터 17시까지인 BestMeetingTimeVo를 반환한다.") @ParameterizedTest @EnumSource(value = Duration.class, names = {"HALF", "HOUR", "HOUR_HALF", "TWO_HOUR"}) void durationsTest(Duration duration) { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_14_00, 0); - BestMeetingTimeVo r2 = new BestMeetingTimeVo(availableDate2, SLOT_14_30, SLOT_17_00, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_14_00, 0, List.of(1L, 2L)); + BestMeetingTimeVo r2 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 11), SLOT_14_30, SLOT_17_00, 0, List.of(1L, 2L)); List result = List.of(r1, r2); // when @@ -216,7 +239,7 @@ void durationsTest(Duration duration) { @EnumSource(value = Duration.class, names = {"TWO_HOUR_HALF"}) void durationTwoHourHalfTest(Duration duration) { // given - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate2, SLOT_14_30, SLOT_17_00, 0); + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 11), SLOT_14_30, SLOT_17_00, 0, List.of(1L, 2L)); List result = List.of(r1); // when @@ -245,19 +268,16 @@ class SumWeightTest { @Test void weightTest() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - LocalDate availableDate2 = LocalDate.of(2023, 7, 11); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_13_30, 4, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate2, SLOT_16_00, SLOT_16_00, 2, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - }}; - - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_14_00, 4); - BestMeetingTimeVo r2 = new BestMeetingTimeVo(availableDate2, SLOT_16_00, SLOT_16_30, 2); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 4, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 4, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 4, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 4, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_16_00, 2, List.of(1L, 2L)) + ); + + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_14_00, 4, List.of(1L, 2L)); + BestMeetingTimeVo r2 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 11), SLOT_16_00, SLOT_16_30, 2, List.of(1L, 2L)); List result = List.of(r1, r2); // when @@ -271,24 +291,18 @@ void weightTest() { @Test void weightTest2() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - LocalDate availableDate2 = LocalDate.of(2023, 7, 11); - LocalDate availableDate3 = LocalDate.of(2023, 7, 12); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_12_00, SLOT_13_30, 2, 2L); - List tempTimeBlocks2 = TimeBlockDtoGenerator - .generator(availableDate2, SLOT_16_00, SLOT_16_00, 4, 2L); - List tempTimeBlocks3 = TimeBlockDtoGenerator - .generator(availableDate3, SLOT_16_30, SLOT_16_30, 1, 2L); - List timeBlocks = new ArrayList<>() {{ - addAll(tempTimeBlocks); - addAll(tempTimeBlocks2); - addAll(tempTimeBlocks3); - }}; - - BestMeetingTimeVo r1 = new BestMeetingTimeVo(availableDate, SLOT_12_00, SLOT_14_00, 2); - BestMeetingTimeVo r2 = new BestMeetingTimeVo(availableDate2, SLOT_16_00, SLOT_16_30, 4); - BestMeetingTimeVo r3 = new BestMeetingTimeVo(availableDate3, SLOT_16_30, SLOT_17_00, 1); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_00, 2, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_12_30, 2, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_00, 2, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_13_30, 2, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 11), TimeSlot.SLOT_16_00, 4, List.of(1L, 2L)), + new TimeBlockVo(LocalDate.of(2024, 7, 12), TimeSlot.SLOT_16_30, 1, List.of(1L, 2L)) + ); + + BestMeetingTimeVo r1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_12_00, SLOT_14_00, 2, List.of(1L, 2L)); + BestMeetingTimeVo r2 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 11), SLOT_16_00, SLOT_16_30, 4, List.of(1L, 2L)); + BestMeetingTimeVo r3 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 12), SLOT_16_30, SLOT_17_00, 1, List.of(1L, 2L)); List result = List.of(r2, r1, r3); // when @@ -306,12 +320,11 @@ class TimeSlotEdgeTest { @Test void timeSlot6_00Test() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_6_00, SLOT_6_00, 0, 2L); - List timeBlocks = new ArrayList<>(tempTimeBlocks); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_6_00, 0, List.of(1L, 2L)) + ); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_6_00, SLOT_6_30, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_6_00, SLOT_6_30, 0, List.of(1L, 2L)); List expected = List.of(e1); // when @@ -325,12 +338,11 @@ void timeSlot6_00Test() { @Test void timeSlot24_00Test() { // given - LocalDate availableDate = LocalDate.of(2023, 7, 10); - List tempTimeBlocks = TimeBlockDtoGenerator - .generator(availableDate, SLOT_23_30, SLOT_23_30, 0, 2L); - List timeBlocks = new ArrayList<>(tempTimeBlocks); + List timeBlocks = List.of( + new TimeBlockVo(LocalDate.of(2024, 7, 10), TimeSlot.SLOT_23_30, 0, List.of(1L, 2L)) + ); - BestMeetingTimeVo e1 = new BestMeetingTimeVo(availableDate, SLOT_23_30, SLOT_24_00, 0); + BestMeetingTimeVo e1 = new BestMeetingTimeVo(LocalDate.of(2024, 7, 10), SLOT_23_30, SLOT_24_00, 0, List.of(1L, 2L)); List expected = List.of(e1); // when diff --git a/src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/MeetingTimeCasesStrategyTest.java b/src/test/java/com/asap/server/service/time/strategy/impl/MeetingTimeCasesStrategyTest.java similarity index 99% rename from src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/MeetingTimeCasesStrategyTest.java rename to src/test/java/com/asap/server/service/time/strategy/impl/MeetingTimeCasesStrategyTest.java index fdf0e12d..99cf8896 100644 --- a/src/test/java/com/asap/server/service/meeting/recommend/strategy/impl/MeetingTimeCasesStrategyTest.java +++ b/src/test/java/com/asap/server/service/time/strategy/impl/MeetingTimeCasesStrategyTest.java @@ -1,9 +1,9 @@ -package com.asap.server.service.meeting.recommend.strategy.impl; +package com.asap.server.service.time.strategy.impl; import com.asap.server.persistence.domain.enums.Duration; -import com.asap.server.service.meeting.recommend.strategy.MeetingTimeCasesStrategy; -import com.asap.server.service.vo.PossibleTimeCaseVo; +import com.asap.server.service.time.strategy.MeetingTimeCasesStrategy; +import com.asap.server.service.time.vo.PossibleTimeCaseVo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/asap/server/service/user/UserLoginServiceTest.java b/src/test/java/com/asap/server/service/user/UserLoginServiceTest.java new file mode 100644 index 00000000..0ea7df72 --- /dev/null +++ b/src/test/java/com/asap/server/service/user/UserLoginServiceTest.java @@ -0,0 +1,225 @@ +package com.asap.server.service.user; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import com.asap.server.common.exception.model.BadRequestException; +import com.asap.server.common.exception.model.ConflictException; +import com.asap.server.common.exception.model.HostTimeForbiddenException; +import com.asap.server.common.exception.model.UnauthorizedException; +import com.asap.server.common.jwt.JwtService; +import com.asap.server.persistence.domain.ConfirmedDateTime; +import com.asap.server.persistence.domain.Meeting; +import com.asap.server.persistence.domain.enums.Role; +import com.asap.server.persistence.domain.user.Name; +import com.asap.server.persistence.domain.user.User; +import com.asap.server.persistence.repository.meeting.MeetingRepository; +import com.asap.server.service.time.UserMeetingScheduleService; +import java.time.LocalDateTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class UserLoginServiceTest { + @Mock + private MeetingRepository meetingRepository; + @Mock + private JwtService jwtService; + @Mock + private UserMeetingScheduleService userMeetingScheduleService; + private final PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + UserLoginService userLoginService; + + @BeforeEach + void setUp() { + userLoginService = new UserLoginService( + meetingRepository, + userMeetingScheduleService, + jwtService, + passwordEncoder + ); + } + + @DisplayName("아직 확정되지 않은 특정 회의의 방장의 이름과 비밀번호가 일치하면 accessToken을 반환한다.") + @Test + void test() { + // given + long meetingId = 1L; + String encodedPassword = passwordEncoder.encode("0000"); + Meeting meeting = Meeting.builder() + .id(meetingId) + .password(encodedPassword) + .build(); + Name name = new Name("KWY"); + User host = User.builder() + .id(1L) + .meeting(meeting) + .name(name) + .role(Role.HOST) + .isFixed(false) + .build(); + meeting.setHost(host); + when(meetingRepository.findByIdWithHost(meetingId)).thenReturn(Optional.of(meeting)); + when(userMeetingScheduleService.isEmptyHostTimeBlock(host.getId())).thenReturn(false); + when(jwtService.issuedToken("1")).thenReturn("access token"); + + String expected = "access token"; + + // when + String response = userLoginService.loginByHost(meetingId, "KWY", "0000"); + + // then + assertThat(response).isEqualTo(expected); + } + + @DisplayName("특정 회의의 방장 이름과 일치하지 않는다면 UnauthorizedException 에러를 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"user1", "user2", "K"}) + void test2(String name) { + // given + long meetingId = 1L; + String encodedPassword = passwordEncoder.encode("0000"); + final Meeting meeting = Meeting.builder() + .id(meetingId) + .password(encodedPassword) + .build(); + Name hostName = new Name("KWY"); + final User host = User.builder() + .id(1L) + .meeting(meeting) + .name(hostName) + .role(Role.HOST) + .isFixed(false) + .build(); + meeting.setHost(host); + when(meetingRepository.findByIdWithHost(meetingId)).thenReturn(Optional.of(meeting)); + + // when, then + assertThatThrownBy(() -> { + userLoginService.loginByHost(meetingId, name, "0000"); + }).isInstanceOf(UnauthorizedException.class); + } + + @DisplayName("특정 회의의 비밀번호가 일치하지 않는다면 UnauthorizedException 에러를 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"1111", "1112", "1234"}) + void test3(String password) { + // given + long meetingId = 1L; + String encodedPassword = passwordEncoder.encode("0000"); + final Meeting meeting = Meeting.builder() + .id(meetingId) + .password(encodedPassword) + .build(); + Name hostName = new Name("KWY"); + final User host = User.builder() + .id(1L) + .meeting(meeting) + .name(hostName) + .role(Role.HOST) + .isFixed(false) + .build(); + meeting.setHost(host); + when(meetingRepository.findByIdWithHost(meetingId)).thenReturn(Optional.of(meeting)); + + // when, then + assertThatThrownBy(() -> { + userLoginService.loginByHost(meetingId, "KWY", password); + }).isInstanceOf(UnauthorizedException.class); + } + + @DisplayName("이미 확정된 회의라면 ConflictException 에러를 반환한다.") + @Test + void test4() { + // given + long meetingId = 1L; + String encodedPassword = passwordEncoder.encode("0000"); + final Meeting meeting = Meeting.builder() + .id(meetingId) + .password(encodedPassword) + .confirmedDateTime(new ConfirmedDateTime(LocalDateTime.now(), LocalDateTime.now())) + .build(); + Name hostName = new Name("KWY"); + final User host = User.builder() + .id(1L) + .meeting(meeting) + .name(hostName) + .role(Role.HOST) + .isFixed(false) + .build(); + meeting.setHost(host); + when(meetingRepository.findByIdWithHost(meetingId)).thenReturn(Optional.of(meeting)); + + // when, then + assertThatThrownBy(() -> { + userLoginService.loginByHost(meetingId, "KWY", "0000"); + }).isInstanceOf(ConflictException.class); + } + + @DisplayName("방장이 시간을 입력하지 않았다면 HostTimeForbiddenException 에러를 반환한다.") + @Test + void test5() { + // given + long meetingId = 1L; + String encodedPassword = passwordEncoder.encode("0000"); + final Meeting meeting = Meeting.builder() + .id(meetingId) + .password(encodedPassword) + .build(); + Name hostName = new Name("KWY"); + final User host = User.builder() + .id(1L) + .meeting(meeting) + .name(hostName) + .role(Role.HOST) + .isFixed(false) + .build(); + meeting.setHost(host); + when(meetingRepository.findByIdWithHost(meetingId)).thenReturn(Optional.of(meeting)); + when(jwtService.issuedToken("1")).thenReturn("access token"); + when(userMeetingScheduleService.isEmptyHostTimeBlock(host.getId())).thenReturn(true); + + // when, then + assertThatThrownBy(() -> { + userLoginService.loginByHost(meetingId, "KWY", "0000"); + }).isInstanceOf(HostTimeForbiddenException.class); + } + + @DisplayName("유효하지 않은 이름을 입력했다면 BadRequestException 을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"123456789", " ", ""}) + void test6(String name) { + // given + long meetingId = 1L; + String encodedPassword = passwordEncoder.encode("0000"); + final Meeting meeting = Meeting.builder() + .id(meetingId) + .password(encodedPassword) + .build(); + Name hostName = new Name("KWY"); + final User host = User.builder() + .id(1L) + .meeting(meeting) + .name(hostName) + .role(Role.HOST) + .isFixed(false) + .build(); + meeting.setHost(host); + when(meetingRepository.findByIdWithHost(meetingId)).thenReturn(Optional.of(meeting)); + + // when, then + assertThatThrownBy(() -> { + userLoginService.loginByHost(meetingId, name, "0000"); + }).isInstanceOf(BadRequestException.class); + } +} \ No newline at end of file diff --git a/src/test/java/com/asap/server/service/user/UserRetrieveServiceTest.java b/src/test/java/com/asap/server/service/user/UserRetrieveServiceTest.java new file mode 100644 index 00000000..add5be18 --- /dev/null +++ b/src/test/java/com/asap/server/service/user/UserRetrieveServiceTest.java @@ -0,0 +1,61 @@ +package com.asap.server.service.user; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +import com.asap.server.persistence.domain.user.User; +import com.asap.server.persistence.repository.user.UserRepository; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserRetrieveServiceTest { + @Mock + private UserRepository userRepository; + @InjectMocks + private UserRetrieveService userRetrieveService; + + @Test + @DisplayName("사용자가 존재할 때, userId를 키로 갖는 Map을 반환한다.") + void test() { + // given + List users = List.of( + User.builder().id(1L).build(), + User.builder().id(2L).build(), + User.builder().id(3L).build() + ); + when(userRepository.findAllByMeetingId(1L)).thenReturn(users); + Map expected = Map.of( + 1L, User.builder().id(1L).build(), + 2L, User.builder().id(2L).build(), + 3L, User.builder().id(3L).build() + ); + + // when + Map result = userRetrieveService.getUserIdToUserMap(1L); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("사용자가 없을 때, 빈 Map을 반환한다.") + void test2() { + // given + when(userRepository.findAllByMeetingId(1L)).thenReturn(Collections.emptyList()); + Map expected = Map.of(); + + // when + Map result = userRetrieveService.getUserIdToUserMap(1L); + + // then + assertThat(result).isEqualTo(expected); + } +} \ No newline at end of file