From ee44fbe600d3766e354517f1a72256ddcdb7bafa Mon Sep 17 00:00:00 2001 From: Yong woo Song Date: Tue, 30 Jan 2024 21:41:44 +0900 Subject: [PATCH 01/81] [#3] feat: Add ECR push to workflow --- .github/workflows/deploy.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1715c51 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,32 @@ +name: Deploy to ECR + +on: + push: + tags: + - v* + workflow_dispatch: + +jobs: + build: + name: Build Image + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Set RELEASE_VERSION + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + - name: Build, tag, and push image to Amazon ECR + run: | + docker build -t dnd-10th-2-backend-springboot . + docker tag dnd-10th-2-backend-springboot:${{ env.RELEASE_VERSION }} public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:${{ env.RELEASE_VERSION }} + docker push public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:${{ env.RELEASE_VERSION }} + From f0d8dcf2b1fbeb80afc7440398a64b7ee1207925 Mon Sep 17 00:00:00 2001 From: Yong woo Song Date: Tue, 30 Jan 2024 21:50:45 +0900 Subject: [PATCH 02/81] [#3] feat: Add main branch trigger for deploy workflow --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1715c51..2bf2458 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,6 +2,8 @@ name: Deploy to ECR on: push: + branches: + - main tags: - v* workflow_dispatch: From 95fe443477d41ceb9921843ce1bbb6e77550101a Mon Sep 17 00:00:00 2001 From: Yong woo Song Date: Tue, 30 Jan 2024 21:59:01 +0900 Subject: [PATCH 03/81] [#3] chore: Change image version to latest for testing --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2bf2458..4fbaf9d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,6 +29,6 @@ jobs: - name: Build, tag, and push image to Amazon ECR run: | docker build -t dnd-10th-2-backend-springboot . - docker tag dnd-10th-2-backend-springboot:${{ env.RELEASE_VERSION }} public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:${{ env.RELEASE_VERSION }} - docker push public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:${{ env.RELEASE_VERSION }} + docker tag dnd-10th-2-backend-springboot:latest public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest + docker push public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest From 3a688e90e1bce93aaa8533356a74286aca31a88f Mon Sep 17 00:00:00 2001 From: Yong woo Song Date: Wed, 31 Jan 2024 12:49:34 +0900 Subject: [PATCH 04/81] [#3] feat: Add workflow for ecs deploy --- .github/workflows/deploy.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4fbaf9d..04c3093 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,20 +15,40 @@ jobs: steps: - name: Check out code uses: actions/checkout@v2 + - name: Set RELEASE_VERSION run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-2 + - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 + - name: Build, tag, and push image to Amazon ECR run: | docker build -t dnd-10th-2-backend-springboot . docker tag dnd-10th-2-backend-springboot:latest public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest docker push public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: task-definition.json + container-name: public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest + image: ${{ steps.build-image.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: dnd-cluster-service + cluster: dnd-cluster + wait-for-service-stability: true + From 466e1b438d6b3f3790d8b676ef98da52ec696364 Mon Sep 17 00:00:00 2001 From: Yong woo Song Date: Wed, 31 Jan 2024 12:51:54 +0900 Subject: [PATCH 05/81] [#3] feat: Add task-definition for ecs deploy --- task-definition.json | 83 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 task-definition.json diff --git a/task-definition.json b/task-definition.json new file mode 100644 index 0000000..9ad381a --- /dev/null +++ b/task-definition.json @@ -0,0 +1,83 @@ +{ + "taskDefinitionArn": "arn:aws:ecs:us-east-1:067220025245:task-definition/dnd-task:3", + "containerDefinitions": [ + { + "name": "springboot", + "image": "public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest", + "cpu": 0, + "portMappings": [ + { + "name": "springboot-8080-tcp", + "containerPort": 8080, + "hostPort": 8080, + "protocol": "tcp", + "appProtocol": "http" + } + ], + "essential": true, + "environment": [ + { + "name": "SPRING_DATASOURCE_URL", + "value": "jdbc:h2:file:/usr/src/app/data/mydb;DB_CLOSE_ON_EXIT=FALSE" + } + ], + "environmentFiles": [], + "mountPoints": [], + "volumesFrom": [], + "ulimits": [], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-create-group": "true", + "awslogs-group": "/ecs/springboot-task", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "ecs" + }, + "secretOptions": [] + } + } + ], + "family": "dnd-task", + "executionRoleArn": "arn:aws:iam::067220025245:role/ecsTaskExecutionRole", + "networkMode": "awsvpc", + "revision": 3, + "volumes": [], + "status": "ACTIVE", + "requiresAttributes": [ + { + "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" + }, + { + "name": "ecs.capability.execution-role-awslogs" + }, + { + "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" + }, + { + "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18" + }, + { + "name": "ecs.capability.task-eni" + }, + { + "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29" + } + ], + "placementConstraints": [], + "compatibilities": [ + "EC2", + "FARGATE" + ], + "requiresCompatibilities": [ + "FARGATE" + ], + "cpu": "256", + "memory": "512", + "runtimePlatform": { + "cpuArchitecture": "X86_64", + "operatingSystemFamily": "LINUX" + }, + "registeredAt": "2024-01-30T15:50:34.745Z", + "registeredBy": "arn:aws:iam::067220025245:root", + "tags": [] +} From 4ef9673adb5c79e5e5638450ef66367ea0ef595a Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sun, 4 Feb 2024 16:38:15 +0900 Subject: [PATCH 06/81] [#13] refactor: Refactor BaseEntity to include basic audit fields --- .../common/domain/AuditableEntity.java | 23 ++++++++++++++++ .../domain/BaseEntity.java} | 27 ++++++++----------- .../org/dnd/modutimer/timer/domain/Timer.java | 4 +-- .../org/dnd/modutimer/user/domain/User.java | 4 +-- 4 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java rename src/main/java/org/dnd/modutimer/{user/application/AbstractJpaEntity.java => common/domain/BaseEntity.java} (68%) diff --git a/src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java b/src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java new file mode 100644 index 0000000..52742f7 --- /dev/null +++ b/src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java @@ -0,0 +1,23 @@ +package org.dnd.modutimer.common.domain; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@EqualsAndHashCode(of = "id", callSuper = false) // Equals()와 Hashcode() 만들어줌 +@Getter +public class AuditableEntity extends BaseEntity { + + @CreatedBy + @Column(name = "created_by", updatable = false, length = 64) + protected String createdBy; + + @LastModifiedBy + @Column(name = "last_modified_by", length = 64) + protected String lastModifiedBy = ""; +} diff --git a/src/main/java/org/dnd/modutimer/user/application/AbstractJpaEntity.java b/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java similarity index 68% rename from src/main/java/org/dnd/modutimer/user/application/AbstractJpaEntity.java rename to src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java index 7b4e368..625c5cc 100644 --- a/src/main/java/org/dnd/modutimer/user/application/AbstractJpaEntity.java +++ b/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java @@ -1,21 +1,24 @@ -package org.dnd.modutimer.user.application; - -import jakarta.persistence.*; +package org.dnd.modutimer.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; import lombok.EqualsAndHashCode; import lombok.Getter; -import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDateTime; - @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode(of = "id", callSuper = false) // Equals()와 Hashcode() 만들어줌 @Getter -public class AbstractJpaEntity { +public class BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) protected Long id; @@ -23,18 +26,10 @@ public class AbstractJpaEntity { @Column(name = "isDeleted", nullable = false, columnDefinition = "BIT default 0") protected Boolean isDeleted = false; - @CreatedBy - @Column(name = "created_by", updatable = false, length = 64) - protected String createdBy; - @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) protected LocalDateTime createdAt; - @LastModifiedBy - @Column(name = "last_modified_by", length = 64) - protected String lastModifiedBy = ""; - @LastModifiedDate @Column(name = "last_modified_at", nullable = false) protected LocalDateTime lastModifiedAt; diff --git a/src/main/java/org/dnd/modutimer/timer/domain/Timer.java b/src/main/java/org/dnd/modutimer/timer/domain/Timer.java index 93afce1..95d3ec1 100644 --- a/src/main/java/org/dnd/modutimer/timer/domain/Timer.java +++ b/src/main/java/org/dnd/modutimer/timer/domain/Timer.java @@ -12,7 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.dnd.modutimer.user.application.AbstractJpaEntity; +import org.dnd.modutimer.common.domain.AuditableEntity; import org.hibernate.annotations.Where; @Entity @@ -21,7 +21,7 @@ @Table(name = "timer") @AttributeOverride(name = "id", column = @Column(name = "timer_id")) @Where(clause = "is_deleted=false") // 삭제가 되지 않는 것만 조회 -public class Timer extends AbstractJpaEntity { +public class Timer extends AuditableEntity { @Enumerated(EnumType.STRING) private TimerStatus status; diff --git a/src/main/java/org/dnd/modutimer/user/domain/User.java b/src/main/java/org/dnd/modutimer/user/domain/User.java index 5ffdfdd..efbcbe7 100644 --- a/src/main/java/org/dnd/modutimer/user/domain/User.java +++ b/src/main/java/org/dnd/modutimer/user/domain/User.java @@ -10,7 +10,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.dnd.modutimer.user.application.AbstractJpaEntity; +import org.dnd.modutimer.common.domain.BaseEntity; import org.hibernate.annotations.Where; @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -19,7 +19,7 @@ @Table(name = "member") @Where(clause = "is_deleted=false") @AttributeOverride(name = "id", column = @Column(name = "user_id")) -public class User extends AbstractJpaEntity { +public class User extends BaseEntity { @Enumerated(EnumType.STRING) @Column(length = 30, nullable = false) From da9a884dfa15df8f352560c94f86fe20288e1949 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sun, 4 Feb 2024 16:51:59 +0900 Subject: [PATCH 07/81] [#-] refactor: Move utils package to common for better structure --- .../java/org/dnd/modutimer/common/exception/ApiException.java | 2 +- .../dnd/modutimer/common/exception/GlobalExceptionHandler.java | 2 +- .../dnd/modutimer/common/security/JwtAuthenticationFilter.java | 2 +- .../java/org/dnd/modutimer/{ => common}/utils/ApiUtils.java | 2 +- .../dnd/modutimer/{ => common}/utils/FilterResponseUtils.java | 2 +- src/main/java/org/dnd/modutimer/config/SecurityConfig.java | 2 +- .../java/org/dnd/modutimer/user/controller/UserController.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/org/dnd/modutimer/{ => common}/utils/ApiUtils.java (97%) rename src/main/java/org/dnd/modutimer/{ => common}/utils/FilterResponseUtils.java (97%) diff --git a/src/main/java/org/dnd/modutimer/common/exception/ApiException.java b/src/main/java/org/dnd/modutimer/common/exception/ApiException.java index c3b6947..364afee 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/ApiException.java +++ b/src/main/java/org/dnd/modutimer/common/exception/ApiException.java @@ -1,7 +1,7 @@ package org.dnd.modutimer.common.exception; import lombok.Getter; -import org.dnd.modutimer.utils.ApiUtils; +import org.dnd.modutimer.common.utils.ApiUtils; import org.springframework.http.HttpStatus; import java.util.Map; diff --git a/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java b/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java index 6dfc98a..97fd828 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ package org.dnd.modutimer.common.exception; -import org.dnd.modutimer.utils.ApiUtils; +import org.dnd.modutimer.common.utils.ApiUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; diff --git a/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java b/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java index 50848ae..165b88f 100644 --- a/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java @@ -14,7 +14,7 @@ import org.dnd.modutimer.common.exception.InternalServerError; import org.dnd.modutimer.common.exception.UnAuthorizedError; import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.utils.ApiUtils; +import org.dnd.modutimer.common.utils.ApiUtils; import org.dnd.modutimer.user.application.UserFindService; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/src/main/java/org/dnd/modutimer/utils/ApiUtils.java b/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java similarity index 97% rename from src/main/java/org/dnd/modutimer/utils/ApiUtils.java rename to src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java index bf4d10d..c0d85b0 100644 --- a/src/main/java/org/dnd/modutimer/utils/ApiUtils.java +++ b/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.utils; +package org.dnd.modutimer.common.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/org/dnd/modutimer/utils/FilterResponseUtils.java b/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java similarity index 97% rename from src/main/java/org/dnd/modutimer/utils/FilterResponseUtils.java rename to src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java index d44e2ec..5bd5dc7 100644 --- a/src/main/java/org/dnd/modutimer/utils/FilterResponseUtils.java +++ b/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.utils; +package org.dnd.modutimer.common.utils; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/org/dnd/modutimer/config/SecurityConfig.java b/src/main/java/org/dnd/modutimer/config/SecurityConfig.java index bea9051..c34b646 100644 --- a/src/main/java/org/dnd/modutimer/config/SecurityConfig.java +++ b/src/main/java/org/dnd/modutimer/config/SecurityConfig.java @@ -6,7 +6,7 @@ import org.dnd.modutimer.common.security.CustomAuthenticationEntryPoint; import org.dnd.modutimer.common.security.JwtAuthenticationFilter; import org.dnd.modutimer.user.application.UserFindService; -import org.dnd.modutimer.utils.FilterResponseUtils; +import org.dnd.modutimer.common.utils.FilterResponseUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/org/dnd/modutimer/user/controller/UserController.java b/src/main/java/org/dnd/modutimer/user/controller/UserController.java index 3529491..daed7d0 100644 --- a/src/main/java/org/dnd/modutimer/user/controller/UserController.java +++ b/src/main/java/org/dnd/modutimer/user/controller/UserController.java @@ -16,7 +16,7 @@ import org.dnd.modutimer.user.dto.UserLoginRequest; import org.dnd.modutimer.user.dto.UserLoginResponse; import org.dnd.modutimer.user.dto.UserRegisterRequest; -import org.dnd.modutimer.utils.ApiUtils; +import org.dnd.modutimer.common.utils.ApiUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; From 7a6ae4718e45bb189d909fdc8ef3dfe1a0664dd1 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sun, 4 Feb 2024 16:55:46 +0900 Subject: [PATCH 08/81] [#-] style: Apply code formatting according to project conventions --- .../dnd/modutimer/ModutimerApplication.java | 6 +-- .../common/exception/ApiException.java | 7 +-- .../common/exception/BadGatewayError.java | 3 +- .../common/exception/BadRequestError.java | 3 +- .../exception/CustomErrorAttributes.java | 2 +- .../common/exception/ForbiddenError.java | 3 +- .../exception/GlobalExceptionHandler.java | 52 ++++++++++--------- .../exception/GlobalValidationHandler.java | 1 + .../common/exception/InternalServerError.java | 3 +- .../common/exception/NotFoundError.java | 3 +- .../common/exception/UnAuthorizedError.java | 3 +- .../CustomAuthenticationEntryPoint.java | 7 +-- .../security/CustomUserDetailsService.java | 2 +- .../common/security/JWTProvider.java | 13 ++--- .../security/JwtAuthenticationFilter.java | 23 ++++---- .../dnd/modutimer/common/utils/ApiUtils.java | 5 +- .../common/utils/FilterResponseUtils.java | 20 +++---- .../dnd/modutimer/config/SwaggerConfig.java | 27 +++++----- .../timer/application/TimerService.java | 17 +++--- .../timer/controller/TimerController.java | 29 ++++++----- .../dnd/modutimer/timer/domain/Duration.java | 17 +++--- .../timer/dto/TimerCreateRequest.java | 9 ++-- .../timer/dto/TimerInfoResponse.java | 19 +++---- .../user/application/UserFindService.java | 4 +- .../user/application/UserService.java | 21 ++++---- .../user/controller/UserController.java | 12 ++--- .../modutimer/user/domain/UserRepository.java | 1 + .../modutimer/user/dto/EmailCheckRequest.java | 1 + .../user/dto/UpdatePasswordRequest.java | 1 + .../modutimer/user/dto/UserInfoResponse.java | 9 ++-- .../modutimer/user/dto/UserLoginRequest.java | 1 + .../modutimer/user/dto/UserLoginResponse.java | 1 + .../user/dto/UserRegisterRequest.java | 12 ++--- .../modutimer/ModutimerApplicationTests.java | 6 +-- 34 files changed, 184 insertions(+), 159 deletions(-) diff --git a/src/main/java/org/dnd/modutimer/ModutimerApplication.java b/src/main/java/org/dnd/modutimer/ModutimerApplication.java index b5fa848..6e614bd 100644 --- a/src/main/java/org/dnd/modutimer/ModutimerApplication.java +++ b/src/main/java/org/dnd/modutimer/ModutimerApplication.java @@ -9,8 +9,8 @@ @EnableJpaAuditing public class ModutimerApplication { - public static void main(String[] args) { - SpringApplication.run(ModutimerApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(ModutimerApplication.class, args); + } } diff --git a/src/main/java/org/dnd/modutimer/common/exception/ApiException.java b/src/main/java/org/dnd/modutimer/common/exception/ApiException.java index 364afee..c0fe88b 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/ApiException.java +++ b/src/main/java/org/dnd/modutimer/common/exception/ApiException.java @@ -22,13 +22,14 @@ public ApiException(ErrorCode errorCode, Map errors, HttpStatus public ApiUtils.ApiResult body() { return ApiUtils.error( - String.valueOf(this.getStatus().value()), - String.valueOf(this.errorCode.getCode()), - this.errors + String.valueOf(this.getStatus().value()), + String.valueOf(this.errorCode.getCode()), + this.errors ); } public interface ErrorCode { + int getCode(); String getMessage(); diff --git a/src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java b/src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java index a9995f4..dee874f 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java +++ b/src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java @@ -6,8 +6,7 @@ import java.util.Map; /** - * HTTP 상태 코드 502 (Bad Gateway) - * 게이트웨이나 프록시 역할을 하는 서버가 상위 서버로부터 잘못된 응답을 받았을 때 발생합니다. + * HTTP 상태 코드 502 (Bad Gateway) 게이트웨이나 프록시 역할을 하는 서버가 상위 서버로부터 잘못된 응답을 받았을 때 발생합니다. */ @Getter public class BadGatewayError extends ApiException { diff --git a/src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java b/src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java index 9508ea0..8b0f773 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java +++ b/src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java @@ -6,8 +6,7 @@ import java.util.Map; /** - * HTTP 상태 코드 400 (Bad Request) : 잘못된 요청 - * 유효성 검사 실패 또는 잘못된 파라미터 요청시 발생합니다. + * HTTP 상태 코드 400 (Bad Request) : 잘못된 요청 유효성 검사 실패 또는 잘못된 파라미터 요청시 발생합니다. */ @Getter diff --git a/src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java b/src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java index 9170d62..11fd56c 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java +++ b/src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java @@ -13,7 +13,7 @@ public class CustomErrorAttributes extends DefaultErrorAttributes { @Override public Map getErrorAttributes( - WebRequest webRequest, ErrorAttributeOptions options) { + WebRequest webRequest, ErrorAttributeOptions options) { Map defaultErrorAttributes = super.getErrorAttributes(webRequest, options); Map customErrorAttributes = new LinkedHashMap<>(); diff --git a/src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java b/src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java index 2da76b0..67f3e71 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java +++ b/src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java @@ -7,8 +7,7 @@ import java.util.Map; /** - * HTTP 상태 코드 403 (Forbidden) : 금지됨 - * 인증은 되었지만, 리소스에 접근할 권한이 없을때 발생합니다. + * HTTP 상태 코드 403 (Forbidden) : 금지됨 인증은 되었지만, 리소스에 접근할 권한이 없을때 발생합니다. */ @Getter public class ForbiddenError extends ApiException { diff --git a/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java b/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java index 97fd828..7300767 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java @@ -31,9 +31,9 @@ public ResponseEntity> handleIllegalArgumentException(Ille BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message ); return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); @@ -43,14 +43,14 @@ public ResponseEntity> handleIllegalArgumentException(Ille public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e) { Map message = new HashMap<>(); // 맵으로 변경 String errorMessage = String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", - e.getName(), e.getValue(), e.getRequiredType().getSimpleName()); + e.getName(), e.getValue(), e.getRequiredType().getSimpleName()); message.put("error", errorMessage); BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message ); return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); @@ -60,13 +60,13 @@ public ResponseEntity> handleTypeMismatch(MethodArgumentTy public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { Map messages = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> - messages.put(error.getField(), error.getDefaultMessage())); + messages.put(error.getField(), error.getDefaultMessage())); BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.VALIDATION_FAILED; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - messages + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + messages ); return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); @@ -75,28 +75,30 @@ public ResponseEntity> handleValidationExceptions(MethodAr @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity> handleMissingParams(MissingServletRequestParameterException ex) { Map message = new HashMap<>(); - message.put("error", String.format("The required parameter '%s' of type '%s' is missing", ex.getParameterName(), ex.getParameterType())); + message.put("error", String.format("The required parameter '%s' of type '%s' is missing", ex.getParameterName(), + ex.getParameterType())); BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.MISSING_PART; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message ); return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); } @ExceptionHandler(MissingServletRequestPartException.class) - public ResponseEntity> handleMissingServletRequestPartException(MissingServletRequestPartException e) { + public ResponseEntity> handleMissingServletRequestPartException( + MissingServletRequestPartException e) { Map message = new HashMap<>(); message.put("error", e.getMessage()); BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.MISSING_PART; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message ); return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); @@ -109,9 +111,9 @@ public ResponseEntity> unknownServerError(Exception e) { InternalServerError.ErrorCode errorCode = InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), - String.valueOf(errorCode.getCode()), - message + String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), + String.valueOf(errorCode.getCode()), + message ); return new ResponseEntity<>(errorResult, HttpStatus.INTERNAL_SERVER_ERROR); @@ -124,9 +126,9 @@ public ResponseEntity> handleHttpMessageNotReadable(HttpMe BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message ); return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); diff --git a/src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java b/src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java index 21220c5..33f0e4b 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java +++ b/src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java @@ -15,6 +15,7 @@ @Aspect @Component public class GlobalValidationHandler { + @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping)") public void postOrPutMapping() { // Post 또는 Put 요청시 공통 로직 처리(AOP) } diff --git a/src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java b/src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java index 9f2ef21..f663487 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java +++ b/src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java @@ -7,8 +7,7 @@ import java.util.Map; /** - * HTTP 상태 코드 500 (Internal Server Error) : 내부 서버 오류 - * 서버에 에러가 발생할 때 발생합니다. + * HTTP 상태 코드 500 (Internal Server Error) : 내부 서버 오류 서버에 에러가 발생할 때 발생합니다. */ @Getter public class InternalServerError extends ApiException { diff --git a/src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java b/src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java index 6a75e1c..464cd24 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java +++ b/src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java @@ -7,8 +7,7 @@ import java.util.Map; /** - * HTTP 상태 코드 404 (Not Found) - * 리소스 찾을 수 없을 때 발생합니다. + * HTTP 상태 코드 404 (Not Found) 리소스 찾을 수 없을 때 발생합니다. */ @Getter public class NotFoundError extends ApiException { diff --git a/src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java b/src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java index 261bf21..10670c9 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java +++ b/src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java @@ -7,8 +7,7 @@ import java.util.Map; /** - * HTTP 상태 코드 401 (Unauthorized) : 권한 없음 - * 인증이 되지 않았을때 발생합니다. + * HTTP 상태 코드 401 (Unauthorized) : 권한 없음 인증이 되지 않았을때 발생합니다. */ @Getter public class UnAuthorizedError extends ApiException { diff --git a/src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java b/src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java index 898e0b5..1309024 100644 --- a/src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java +++ b/src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java @@ -16,10 +16,11 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { UnAuthorizedError errorResponse = new UnAuthorizedError( - UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, - Collections.singletonMap("auth", "Not authenticated") + UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, + Collections.singletonMap("auth", "Not authenticated") ); response.setContentType(MediaType.APPLICATION_JSON_VALUE); diff --git a/src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java b/src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java index 2f7cbe5..61a0f45 100644 --- a/src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java +++ b/src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java @@ -26,7 +26,7 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException(String.format("User with email: %s not found.", email))); + .orElseThrow(() -> new UsernameNotFoundException(String.format("User with email: %s not found.", email))); return new CustomUserDetails(user); } diff --git a/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java b/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java index 43e17e1..0f703af 100644 --- a/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java +++ b/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java @@ -13,6 +13,7 @@ @Component public class JWTProvider { + public static final Long EXP = 1000L * 60 * 60 * 48; // 48시간 public static final String TOKEN_PREFIX = "Bearer "; public static final String HEADER = "Authorization"; @@ -20,11 +21,11 @@ public class JWTProvider { public static String create(User user) { String jwt = JWT.create() - .withSubject(user.getEmail()) - .withExpiresAt(new Date(System.currentTimeMillis() + EXP)) - .withClaim("id", user.getId()) - .withClaim("role", user.getRole().name()) - .sign(Algorithm.HMAC512(SECRET)); + .withSubject(user.getEmail()) + .withExpiresAt(new Date(System.currentTimeMillis() + EXP)) + .withClaim("id", user.getId()) + .withClaim("role", user.getRole().name()) + .sign(Algorithm.HMAC512(SECRET)); return TOKEN_PREFIX + jwt; } @@ -32,7 +33,7 @@ public static String create(User user) { public static DecodedJWT verify(String jwt) throws SignatureVerificationException, TokenExpiredException { jwt = jwt.replace(TOKEN_PREFIX, ""); DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(SECRET)) - .build().verify(jwt); + .build().verify(jwt); return decodedJWT; } diff --git a/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java b/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java index 165b88f..39208a0 100644 --- a/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java @@ -39,7 +39,8 @@ public JwtAuthenticationFilter(AuthenticationManager authenticationManager, User } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { String jwt = request.getHeader(JWTProvider.HEADER); try { @@ -51,26 +52,30 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse CustomUserDetails myUserDetails = new CustomUserDetails(user); Authentication authentication = - new UsernamePasswordAuthenticationToken( - myUserDetails, - null, - myUserDetails.getAuthorities() - ); + new UsernamePasswordAuthenticationToken( + myUserDetails, + null, + myUserDetails.getAuthorities() + ); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (SignatureVerificationException sve) { - handleException(response, new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, Collections.singletonMap("defaultMessage", "Invalid token signature"))); + handleException(response, new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("defaultMessage", "Invalid token signature"))); return; } catch (TokenExpiredException tee) { - handleException(response, new UnAuthorizedError(UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, Collections.singletonMap("defaultMessage", "JWT has expired"))); + handleException(response, new UnAuthorizedError(UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, + Collections.singletonMap("defaultMessage", "JWT has expired"))); return; } catch (Exception e) { - handleException(response, new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, Collections.singletonMap("defaultMessage", "An unexpected error occurred"))); + handleException(response, new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("defaultMessage", "An unexpected error occurred"))); return; } chain.doFilter(request, response); } + // 인증이 필요하지 않는 url private boolean isNonProtectedUrl(HttpServletRequest request) { for (String urlPattern : PUBLIC_URLS) { diff --git a/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java b/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java index c0d85b0..12b5b07 100644 --- a/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java +++ b/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java @@ -9,8 +9,7 @@ import java.util.Map; /** - * API 응답을 생성하는데 사용되는 유틸리티 메서드와 내부 클래스 제공 - * 성공적인 API 응답과 에러 응답을 생성하는 정적 메서드를 포함 + * API 응답을 생성하는데 사용되는 유틸리티 메서드와 내부 클래스 제공 성공적인 API 응답과 에러 응답을 생성하는 정적 메서드를 포함 */ public class ApiUtils { @@ -27,6 +26,7 @@ public static ApiResult error(String status, String code, Map @Setter @AllArgsConstructor public static class ApiResult { + private final boolean success; private final T response; private final ApiError error; @@ -46,6 +46,7 @@ public String toString() { @Setter @AllArgsConstructor public static class ApiError { + private final String status; private final String code; private final Map message; diff --git a/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java b/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java index 5bd5dc7..f0f616b 100644 --- a/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java +++ b/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java @@ -10,9 +10,8 @@ import java.io.IOException; /** - * HTTP 응답을 처리하기 위한 유틸리티 메서드 - * 보안 관련 예외(예: 인증되지 않음, 접근 금지)가 발생했을 때, 적절한 HTTP 응답을 생성하고 클라이언트에 전송하는 역할 수행 - * HTTP 상태 코드, 에러 메시지 등을 JSON 형식으로 변환하여 응답 본문에 포함함 + * HTTP 응답을 처리하기 위한 유틸리티 메서드 보안 관련 예외(예: 인증되지 않음, 접근 금지)가 발생했을 때, 적절한 HTTP 응답을 생성하고 클라이언트에 전송하는 역할 수행 HTTP 상태 코드, 에러 메시지 + * 등을 JSON 형식으로 변환하여 응답 본문에 포함함 */ public class FilterResponseUtils { @@ -20,9 +19,9 @@ public class FilterResponseUtils { public static void unAuthorized(HttpServletResponse resp, UnAuthorizedError e) throws IOException { String responseBody = om.writeValueAsString(ApiUtils.error( - String.valueOf(e.getStatus().value()), - String.valueOf(e.getErrorCode().getCode()), - e.getErrors() + String.valueOf(e.getStatus().value()), + String.valueOf(e.getErrorCode().getCode()), + e.getErrors() )); sendResponse(resp, responseBody, e.getStatus()); @@ -30,15 +29,16 @@ public static void unAuthorized(HttpServletResponse resp, UnAuthorizedError e) t public static void forbidden(HttpServletResponse resp, ForbiddenError e) throws IOException { String responseBody = om.writeValueAsString(ApiUtils.error( - String.valueOf(e.getStatus().value()), - String.valueOf(e.getErrorCode().getCode()), - e.getErrors() + String.valueOf(e.getStatus().value()), + String.valueOf(e.getErrorCode().getCode()), + e.getErrors() )); sendResponse(resp, responseBody, e.getStatus()); } - private static void sendResponse(HttpServletResponse resp, String responseBody, HttpStatus status) throws IOException { + private static void sendResponse(HttpServletResponse resp, String responseBody, HttpStatus status) + throws IOException { resp.getWriter().println(responseBody); resp.setStatus(status.value()); resp.setContentType("application/json; charset=utf-8"); diff --git a/src/main/java/org/dnd/modutimer/config/SwaggerConfig.java b/src/main/java/org/dnd/modutimer/config/SwaggerConfig.java index 0633959..13d6907 100644 --- a/src/main/java/org/dnd/modutimer/config/SwaggerConfig.java +++ b/src/main/java/org/dnd/modutimer/config/SwaggerConfig.java @@ -10,30 +10,31 @@ import org.springframework.context.annotation.Configuration; @OpenAPIDefinition( - info = @io.swagger.v3.oas.annotations.info.Info(title = "ModuTimer API 명세서", - description = "모두의 타이머 API 명세서", - version = "v1")) + info = @io.swagger.v3.oas.annotations.info.Info(title = "ModuTimer API 명세서", + description = "모두의 타이머 API 명세서", + version = "v1")) @Configuration public class SwaggerConfig { + @Bean public OpenAPI openAPI() { Info info = new Info() - .version("v1.0.0") - .title("API") - .description(""); + .version("v1.0.0") + .title("API") + .description(""); String jwt = "JWT"; SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); // 헤더에 토큰 포함 Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() - .name(jwt) - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") + .name(jwt) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") ); return new OpenAPI() - .info(info) - .addSecurityItem(securityRequirement) - .components(components); + .info(info) + .addSecurityItem(securityRequirement) + .components(components); } } diff --git a/src/main/java/org/dnd/modutimer/timer/application/TimerService.java b/src/main/java/org/dnd/modutimer/timer/application/TimerService.java index d3624d9..f2ee668 100644 --- a/src/main/java/org/dnd/modutimer/timer/application/TimerService.java +++ b/src/main/java/org/dnd/modutimer/timer/application/TimerService.java @@ -1,6 +1,7 @@ package org.dnd.modutimer.timer.application; -import jakarta.persistence.EntityNotFoundException; +import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; import org.dnd.modutimer.common.exception.NotFoundError; import org.dnd.modutimer.timer.domain.Duration; @@ -10,13 +11,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Collections; -import java.util.List; - @Service @RequiredArgsConstructor // final 의존성 주입 @Transactional // DB 변경 작업에 사용 public class TimerService { + private final TimerRepository timerRepository; public Timer createTimer(TimerCreateRequest createDto) { @@ -28,8 +27,8 @@ public Timer createTimer(TimerCreateRequest createDto) { @Transactional(readOnly = true) public Timer findById(Long id) { return timerRepository.findById(id) - .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("TimerId", "Timer not found"))); + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("TimerId", "Timer not found"))); } @Transactional(readOnly = true) @@ -54,9 +53,9 @@ public void changeDuration(Long timerId, Duration newDuration) { public void deleteTimer(Long timerId) { Timer timer = timerRepository.findById(timerId) - .orElseThrow(() -> - new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("TimerId", "Timer not found"))); + .orElseThrow(() -> + new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("TimerId", "Timer not found"))); timer.delete(); // soft 삭제 로직 호출 } } diff --git a/src/main/java/org/dnd/modutimer/timer/controller/TimerController.java b/src/main/java/org/dnd/modutimer/timer/controller/TimerController.java index 60a516b..58329f2 100644 --- a/src/main/java/org/dnd/modutimer/timer/controller/TimerController.java +++ b/src/main/java/org/dnd/modutimer/timer/controller/TimerController.java @@ -3,25 +3,30 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.dnd.modutimer.timer.application.TimerService; import org.dnd.modutimer.timer.domain.Timer; import org.dnd.modutimer.timer.dto.TimerCreateRequest; import org.dnd.modutimer.timer.dto.TimerInfoResponse; import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.net.URI; -import java.util.List; -import java.util.stream.Collectors; - @Tag(name = "Timer 컨트롤러", description = "Timer API입니다.") @RestController @RequestMapping("/api/v1/timers") @RequiredArgsConstructor public class TimerController { + private final TimerService timerService; @GetMapping @@ -29,9 +34,9 @@ public class TimerController { public ResponseEntity getTimers() { // TODO : 해당 User의 타이머만 조회하도록 수정 List timerInfoResponseList = timerService.findAll() - .stream() - .map(TimerInfoResponse::from) - .collect(Collectors.toList()); + .stream() + .map(TimerInfoResponse::from) + .collect(Collectors.toList()); return ResponseEntity.ok().body(timerInfoResponseList); } @@ -52,9 +57,9 @@ public ResponseEntity createTimer(@RequestBody @Valid TimerCreateRequest t Timer savedTimer = timerService.createTimer(timerCreateRequest); URI location = ServletUriComponentsBuilder.fromCurrentRequest() - .path("/{id}") - .buildAndExpand(savedTimer.getId()) - .toUri(); // http://localhost:8080/api/v1/timers/123 + .path("/{id}") + .buildAndExpand(savedTimer.getId()) + .toUri(); // http://localhost:8080/api/v1/timers/123 return ResponseEntity.created(location).build(); // 201(Created) 상태코드 + URI 반환 } diff --git a/src/main/java/org/dnd/modutimer/timer/domain/Duration.java b/src/main/java/org/dnd/modutimer/timer/domain/Duration.java index 9f86a5b..f6351ed 100644 --- a/src/main/java/org/dnd/modutimer/timer/domain/Duration.java +++ b/src/main/java/org/dnd/modutimer/timer/domain/Duration.java @@ -11,6 +11,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Duration { // VO(값 객체) + private long startTime; private long endTime; @@ -28,8 +29,12 @@ public long toMillis() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Duration)) return false; + if (this == o) { + return true; + } + if (!(o instanceof Duration)) { + return false; + } Duration duration = (Duration) o; return startTime == duration.startTime && endTime == duration.endTime; } @@ -42,10 +47,10 @@ public int hashCode() { @Override public String toString() { return "Duration{" + - "startTime=" + startTime + - ", endTime=" + endTime + - ", durationInMillis=" + toMillis() + - '}'; + "startTime=" + startTime + + ", endTime=" + endTime + + ", durationInMillis=" + toMillis() + + '}'; } } diff --git a/src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java b/src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java index 663d14f..315533e 100644 --- a/src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java +++ b/src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java @@ -17,6 +17,7 @@ @Setter @NoArgsConstructor public class TimerCreateRequest { + @NotNull(message = "시작 시간은 반드시 입력되어야 합니다") @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") // Format : 2021-12-31T15:30:45 @Schema(description = "타이머 시작시간", example = "2024-01-11T15:30:45") @@ -33,9 +34,9 @@ public class TimerCreateRequest { public Timer toEntity() { return Timer.builder() - .duration(new Duration(startTime.toInstant(ZoneOffset.UTC).toEpochMilli(), - endTime.toInstant(ZoneOffset.UTC).toEpochMilli())) - .status(this.status) - .build(); + .duration(new Duration(startTime.toInstant(ZoneOffset.UTC).toEpochMilli(), + endTime.toInstant(ZoneOffset.UTC).toEpochMilli())) + .status(this.status) + .build(); } } diff --git a/src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java b/src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java index e2fad33..db911dd 100644 --- a/src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java +++ b/src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java @@ -10,6 +10,7 @@ @Getter @Setter public class TimerInfoResponse { + @Schema(description = "타이머 id", example = "12L") private Long id; @Schema(description = "타이머 상태", example = "STOPPED") @@ -21,10 +22,10 @@ public class TimerInfoResponse { @Builder public TimerInfoResponse( - final Long id, - final TimerStatus status, - final long startTime, - final long endTime + final Long id, + final TimerStatus status, + final long startTime, + final long endTime ) { this.id = id; this.status = status; @@ -34,11 +35,11 @@ public TimerInfoResponse( public static TimerInfoResponse from(Timer timer) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 return TimerInfoResponse.builder() - .id(timer.getId()) - .status(timer.getStatus()) - .startTime(timer.getDuration().getStartTime()) - .endTime(timer.getDuration().getEndTime()) - .build(); + .id(timer.getId()) + .status(timer.getStatus()) + .startTime(timer.getDuration().getStartTime()) + .endTime(timer.getDuration().getEndTime()) + .build(); } } diff --git a/src/main/java/org/dnd/modutimer/user/application/UserFindService.java b/src/main/java/org/dnd/modutimer/user/application/UserFindService.java index f29c5e9..18571ad 100644 --- a/src/main/java/org/dnd/modutimer/user/application/UserFindService.java +++ b/src/main/java/org/dnd/modutimer/user/application/UserFindService.java @@ -20,7 +20,7 @@ public UserFindService(UserRepository userRepository) { public User getUserById(Long id) throws Exception { return userRepository.findById(id) - .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("User", "User not found"))); + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("User", "User not found"))); } } \ No newline at end of file diff --git a/src/main/java/org/dnd/modutimer/user/application/UserService.java b/src/main/java/org/dnd/modutimer/user/application/UserService.java index 354f2c1..60c1348 100644 --- a/src/main/java/org/dnd/modutimer/user/application/UserService.java +++ b/src/main/java/org/dnd/modutimer/user/application/UserService.java @@ -23,6 +23,7 @@ @RequiredArgsConstructor @Service public class UserService { + private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; @@ -35,28 +36,29 @@ public void register(UserRegisterRequest userRegisterRequest) { userRepository.save(userRegisterRequest.toEntity(encodedPassword)); } catch (Exception e) { throw new InternalServerError( - InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, - Collections.singletonMap("error", "Unknown server error occurred.")); + InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("error", "Unknown server error occurred.")); } } public void checkSameEmail(String email) { Optional memberOptional = userRepository.findByEmail(email); if (memberOptional.isPresent()) { - throw new BadRequestError(BadRequestError.ErrorCode.DUPLICATE_RESOURCE, Collections.singletonMap("Email", "Duplicate email exist : " + email)); + throw new BadRequestError(BadRequestError.ErrorCode.DUPLICATE_RESOURCE, + Collections.singletonMap("Email", "Duplicate email exist : " + email)); } } public UserLoginResponse login(UserLoginRequest requestDTO) { User user = userRepository.findByEmail(requestDTO.getEmail()).orElseThrow( - () -> new NotFoundError( - NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("Email", "email not found : " + requestDTO.getEmail()) - )); + () -> new NotFoundError( + NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("Email", "email not found : " + requestDTO.getEmail()) + )); if (!passwordEncoder.matches(requestDTO.getPassword(), user.getPassword())) { throw new UnAuthorizedError( - UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, - Collections.singletonMap("Password", "Wrong password") + UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, + Collections.singletonMap("Password", "Wrong password") ); } @@ -66,7 +68,6 @@ public UserLoginResponse login(UserLoginRequest requestDTO) { return new UserLoginResponse(jwt, redirectUrl); } - // public UserInfoResponse findUser(User user) { // User findUser = userRepository.findById(user.getId()) // .orElseThrow(() -> new NotFoundError( diff --git a/src/main/java/org/dnd/modutimer/user/controller/UserController.java b/src/main/java/org/dnd/modutimer/user/controller/UserController.java index daed7d0..866771e 100644 --- a/src/main/java/org/dnd/modutimer/user/controller/UserController.java +++ b/src/main/java/org/dnd/modutimer/user/controller/UserController.java @@ -34,9 +34,9 @@ public class UserController { @PostMapping("/check") @Operation(summary = "이메일 중복 검사", description = "입력받은 이메일 주소가 이미 사용 중인지 확인한다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "이메일 사용 가능"), - @ApiResponse(responseCode = "400", description = "중복된 이메일 존재 또는 잘못된 요청", content = @Content(schema = @Schema(implementation = BadRequestError.class))), - @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = InternalServerError.class))) + @ApiResponse(responseCode = "200", description = "이메일 사용 가능"), + @ApiResponse(responseCode = "400", description = "중복된 이메일 존재 또는 잘못된 요청", content = @Content(schema = @Schema(implementation = BadRequestError.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = InternalServerError.class))) }) public ResponseEntity checkEmail(@RequestBody @Valid EmailCheckRequest emailCheckDTO) { userService.checkSameEmail(emailCheckDTO.getEmail()); @@ -46,9 +46,9 @@ public ResponseEntity checkEmail(@RequestBody @Valid EmailCheckRequest emailC @PostMapping @ApiResponses({ - @ApiResponse(responseCode = "200", description = "유저 등록 성공"), - @ApiResponse(responseCode = "400", description = "중복된 이메일 존재 또는 잘못된 요청", content = @Content(schema = @Schema(implementation = BadRequestError.class))), - @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = InternalServerError.class))) + @ApiResponse(responseCode = "200", description = "유저 등록 성공"), + @ApiResponse(responseCode = "400", description = "중복된 이메일 존재 또는 잘못된 요청", content = @Content(schema = @Schema(implementation = BadRequestError.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = InternalServerError.class))) }) @Operation(summary = "유저 등록", description = "새롭게 유저를 등록한다.") public ResponseEntity registerUser(@RequestBody @Valid UserRegisterRequest requestDTO) { diff --git a/src/main/java/org/dnd/modutimer/user/domain/UserRepository.java b/src/main/java/org/dnd/modutimer/user/domain/UserRepository.java index 8da3362..3a0d263 100644 --- a/src/main/java/org/dnd/modutimer/user/domain/UserRepository.java +++ b/src/main/java/org/dnd/modutimer/user/domain/UserRepository.java @@ -6,5 +6,6 @@ import java.util.Optional; public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java b/src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java index ce65bdc..d3cab5a 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java +++ b/src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java @@ -9,6 +9,7 @@ @Getter @Setter public class EmailCheckRequest { + @NotBlank(message = "email is required.") @Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "Please enter a valid email address") @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") diff --git a/src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java b/src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java index 9b38294..f455cc1 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java +++ b/src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java @@ -10,6 +10,7 @@ @Getter @Setter public class UpdatePasswordRequest { + @NotBlank(message = "password is required.") @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters") @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=!~`<>,./?;:'\"\\[\\]{}\\\\()|_-])\\S*$", message = "It must contain letters, numbers, and special characters, and cannot contain spaces") diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java b/src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java index edb1f48..9d01f94 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java +++ b/src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java @@ -9,6 +9,7 @@ @Getter @Setter public class UserInfoResponse { + @Schema(description = "사용자 id", nullable = false, example = "12") private long id; @Schema(description = "사용자 이름", nullable = false, example = "green12") @@ -28,10 +29,10 @@ public UserInfoResponse(long id, String username, String email, UserRole role) { public static UserInfoResponse from(User user) { return new UserInfoResponse( - user.getId(), - user.getName(), - user.getEmail(), - user.getRole() + user.getId(), + user.getName(), + user.getEmail(), + user.getRole() ); } } diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java b/src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java index e8c88d1..db5872d 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java +++ b/src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java @@ -10,6 +10,7 @@ @Getter @Setter public class UserLoginRequest { + @NotBlank(message = "email is required.") @Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "Please enter a valid email address.") @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java b/src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java index 4da8fb6..6867b55 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java +++ b/src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java @@ -6,6 +6,7 @@ @Getter @Setter public class UserLoginResponse { + private String jwtToken; private String redirectUrl; diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java b/src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java index 87ea3e1..a948a64 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java +++ b/src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java @@ -12,6 +12,7 @@ @Getter @Setter public class UserRegisterRequest { + @NotBlank(message = "email is required.") @Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "Please enter a valid email address") @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") @@ -29,13 +30,12 @@ public class UserRegisterRequest { private String name; - public User toEntity(String encodedPassword) { return User.builder() - .email(email) - .password(encodedPassword) - .name(name) - .role(UserRole.ROLE_USER) // 기본적으로 User로 생성 - .build(); + .email(email) + .password(encodedPassword) + .name(name) + .role(UserRole.ROLE_USER) // 기본적으로 User로 생성 + .build(); } } diff --git a/src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java b/src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java index 1064116..598a922 100644 --- a/src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java +++ b/src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java @@ -8,8 +8,8 @@ @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) class ModutimerApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } From e456e0dfc62ace8a96ba28e3e57b9040d0e252ff Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sun, 4 Feb 2024 17:07:16 +0900 Subject: [PATCH 09/81] [#13] refactor: Rename isDeleted column to is_deleted for SQL naming conventions --- src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java b/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java index 625c5cc..bf0f9ed 100644 --- a/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java +++ b/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java @@ -23,7 +23,7 @@ public class BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) protected Long id; - @Column(name = "isDeleted", nullable = false, columnDefinition = "BIT default 0") + @Column(name = "is_deleted", nullable = false, columnDefinition = "BIT default 0") protected Boolean isDeleted = false; @CreatedDate From 3d3ce6b6fe3a06d59dae24077009cfb355fbca4f Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 4 Feb 2024 22:47:29 +0900 Subject: [PATCH 10/81] chore: Add Jwt secret to env value --- .../java/org/dnd/modutimer/common/security/JWTProvider.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java b/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java index 0f703af..a4d8d7e 100644 --- a/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java +++ b/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java @@ -6,17 +6,19 @@ import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; +import java.util.Date; import org.dnd.modutimer.user.domain.User; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.Date; - @Component public class JWTProvider { public static final Long EXP = 1000L * 60 * 60 * 48; // 48시간 public static final String TOKEN_PREFIX = "Bearer "; public static final String HEADER = "Authorization"; + + @Value("${jwt.secret}") public static final String SECRET = "MySecretKey"; public static String create(User user) { From 25411f70f44e312bc211ebccb99797ed240a8e5e Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Tue, 6 Feb 2024 13:36:03 +0900 Subject: [PATCH 11/81] [#15] refactor: Update project name --- settings.gradle | 2 +- .../exception/GlobalExceptionHandler.java | 137 ------------------ .../TimeetApplication.java} | 6 +- .../common/domain/AuditableEntity.java | 3 +- .../common/domain/BaseEntity.java | 2 +- .../common/exception/ApiException.java | 4 +- .../common/exception/BadGatewayError.java | 2 +- .../common/exception/BadRequestError.java | 2 +- .../exception/CustomErrorAttributes.java | 2 +- .../common/exception/ForbiddenError.java | 2 +- .../exception/GlobalValidationHandler.java | 2 +- .../common/exception/InternalServerError.java | 2 +- .../common/exception/NotFoundError.java | 2 +- .../common/exception/UnAuthorizedError.java | 2 +- .../CustomAuthenticationEntryPoint.java | 4 +- .../common/security/CustomUserDetails.java | 4 +- .../security/CustomUserDetailsService.java | 6 +- .../common/security/JWTProvider.java | 4 +- .../security/JwtAuthenticationFilter.java | 21 ++- .../common/utils/ApiUtils.java | 2 +- .../common/utils/FilterResponseUtils.java | 6 +- .../config/SecurityConfig.java | 12 +- .../config/SwaggerConfig.java | 4 +- .../timer/application/TimerService.java | 12 +- .../timer/controller/TimerController.java | 10 +- .../timer/domain/Duration.java | 2 +- .../timer/domain/Timer.java | 4 +- .../timer/domain/TimerRepository.java | 2 +- .../timer/domain/TimerStatus.java | 2 +- .../timer/dto/TimerCreateRequest.java | 8 +- .../timer/dto/TimerInfoResponse.java | 6 +- .../user/application/UserFindService.java | 8 +- .../user/application/UserService.java | 22 +-- .../user/controller/UserController.java | 20 +-- .../user/domain/User.java | 4 +- .../user/domain/UserRepository.java | 2 +- .../user/domain/UserRole.java | 2 +- .../user/dto/EmailCheckRequest.java | 2 +- .../user/dto/UpdatePasswordRequest.java | 2 +- .../user/dto/UserInfoResponse.java | 6 +- .../user/dto/UserLoginRequest.java | 2 +- .../user/dto/UserLoginResponse.java | 2 +- .../user/dto/UserRegisterRequest.java | 6 +- .../TimeetApplicationTests.java} | 4 +- 44 files changed, 111 insertions(+), 250 deletions(-) delete mode 100644 src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java rename src/main/java/org/dnd/{modutimer/ModutimerApplication.java => timeet/TimeetApplication.java} (69%) rename src/main/java/org/dnd/{modutimer => timeet}/common/domain/AuditableEntity.java (82%) rename src/main/java/org/dnd/{modutimer => timeet}/common/domain/BaseEntity.java (96%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/ApiException.java (90%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/BadGatewayError.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/BadRequestError.java (96%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/CustomErrorAttributes.java (97%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/ForbiddenError.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/GlobalValidationHandler.java (97%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/InternalServerError.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/NotFoundError.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/common/exception/UnAuthorizedError.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/common/security/CustomAuthenticationEntryPoint.java (91%) rename src/main/java/org/dnd/{modutimer => timeet}/common/security/CustomUserDetails.java (93%) rename src/main/java/org/dnd/{modutimer => timeet}/common/security/CustomUserDetailsService.java (89%) rename src/main/java/org/dnd/{modutimer => timeet}/common/security/JWTProvider.java (93%) rename src/main/java/org/dnd/{modutimer => timeet}/common/security/JwtAuthenticationFilter.java (87%) rename src/main/java/org/dnd/{modutimer => timeet}/common/utils/ApiUtils.java (97%) rename src/main/java/org/dnd/{modutimer => timeet}/common/utils/FilterResponseUtils.java (91%) rename src/main/java/org/dnd/{modutimer => timeet}/config/SecurityConfig.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/config/SwaggerConfig.java (91%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/application/TimerService.java (86%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/controller/TimerController.java (92%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/domain/Duration.java (97%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/domain/Timer.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/domain/TimerRepository.java (81%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/domain/TimerStatus.java (55%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/dto/TimerCreateRequest.java (88%) rename src/main/java/org/dnd/{modutimer => timeet}/timer/dto/TimerInfoResponse.java (90%) rename src/main/java/org/dnd/{modutimer => timeet}/user/application/UserFindService.java (76%) rename src/main/java/org/dnd/{modutimer => timeet}/user/application/UserService.java (81%) rename src/main/java/org/dnd/{modutimer => timeet}/user/controller/UserController.java (84%) rename src/main/java/org/dnd/{modutimer => timeet}/user/domain/User.java (93%) rename src/main/java/org/dnd/{modutimer => timeet}/user/domain/UserRepository.java (84%) rename src/main/java/org/dnd/{modutimer => timeet}/user/domain/UserRole.java (61%) rename src/main/java/org/dnd/{modutimer => timeet}/user/dto/EmailCheckRequest.java (93%) rename src/main/java/org/dnd/{modutimer => timeet}/user/dto/UpdatePasswordRequest.java (95%) rename src/main/java/org/dnd/{modutimer => timeet}/user/dto/UserInfoResponse.java (88%) rename src/main/java/org/dnd/{modutimer => timeet}/user/dto/UserLoginRequest.java (96%) rename src/main/java/org/dnd/{modutimer => timeet}/user/dto/UserLoginResponse.java (89%) rename src/main/java/org/dnd/{modutimer => timeet}/user/dto/UserRegisterRequest.java (92%) rename src/test/java/org/dnd/{modutimer/ModutimerApplicationTests.java => timeet/TimeetApplicationTests.java} (83%) diff --git a/settings.gradle b/settings.gradle index 2c10bbe..393c2a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'modutimer' +rootProject.name = 'timeet' diff --git a/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java b/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java deleted file mode 100644 index 7300767..0000000 --- a/src/main/java/org/dnd/modutimer/common/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.dnd.modutimer.common.exception; - - -import org.dnd.modutimer.common.utils.ApiUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.multipart.support.MissingServletRequestPartException; - -import java.util.HashMap; -import java.util.Map; - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(ApiException.class) - public ResponseEntity> handleApiException(ApiException e) { - - return new ResponseEntity<>(e.body(), e.getStatus()); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { - Map message = new HashMap<>(); - message.put("error", e.getMessage()); - - BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message - ); - - return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e) { - Map message = new HashMap<>(); // 맵으로 변경 - String errorMessage = String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", - e.getName(), e.getValue(), e.getRequiredType().getSimpleName()); - message.put("error", errorMessage); - - BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message - ); - - return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { - Map messages = new HashMap<>(); - ex.getBindingResult().getFieldErrors().forEach(error -> - messages.put(error.getField(), error.getDefaultMessage())); - - BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.VALIDATION_FAILED; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - messages - ); - - return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(MissingServletRequestParameterException.class) - public ResponseEntity> handleMissingParams(MissingServletRequestParameterException ex) { - Map message = new HashMap<>(); - message.put("error", String.format("The required parameter '%s' of type '%s' is missing", ex.getParameterName(), - ex.getParameterType())); - - BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.MISSING_PART; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message - ); - - return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(MissingServletRequestPartException.class) - public ResponseEntity> handleMissingServletRequestPartException( - MissingServletRequestPartException e) { - Map message = new HashMap<>(); - message.put("error", e.getMessage()); - - BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.MISSING_PART; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message - ); - - return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity> unknownServerError(Exception e) { - Map message = new HashMap<>(); - message.put("error", e.getMessage()); - - InternalServerError.ErrorCode errorCode = InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), - String.valueOf(errorCode.getCode()), - message - ); - - return new ResponseEntity<>(errorResult, HttpStatus.INTERNAL_SERVER_ERROR); - } - - @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleHttpMessageNotReadable(HttpMessageNotReadableException e) { - Map message = new HashMap<>(); - message.put("error", "The request body is not readable or has an invalid format."); - - BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; - ApiUtils.ApiResult errorResult = ApiUtils.error( - String.valueOf(HttpStatus.BAD_REQUEST.value()), - String.valueOf(errorCode.getCode()), - message - ); - - return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); - } - -} diff --git a/src/main/java/org/dnd/modutimer/ModutimerApplication.java b/src/main/java/org/dnd/timeet/TimeetApplication.java similarity index 69% rename from src/main/java/org/dnd/modutimer/ModutimerApplication.java rename to src/main/java/org/dnd/timeet/TimeetApplication.java index 6e614bd..f05c8a6 100644 --- a/src/main/java/org/dnd/modutimer/ModutimerApplication.java +++ b/src/main/java/org/dnd/timeet/TimeetApplication.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer; +package org.dnd.timeet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -7,10 +7,10 @@ @SpringBootApplication @EnableJpaAuditing -public class ModutimerApplication { +public class TimeetApplication { public static void main(String[] args) { - SpringApplication.run(ModutimerApplication.class, args); + SpringApplication.run(TimeetApplication.class, args); } } diff --git a/src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java b/src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java similarity index 82% rename from src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java rename to src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java index 52742f7..d55f4fa 100644 --- a/src/main/java/org/dnd/modutimer/common/domain/AuditableEntity.java +++ b/src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.domain; +package org.dnd.timeet.common.domain; import jakarta.persistence.*; import lombok.EqualsAndHashCode; @@ -9,7 +9,6 @@ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -@EqualsAndHashCode(of = "id", callSuper = false) // Equals()와 Hashcode() 만들어줌 @Getter public class AuditableEntity extends BaseEntity { diff --git a/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java b/src/main/java/org/dnd/timeet/common/domain/BaseEntity.java similarity index 96% rename from src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java rename to src/main/java/org/dnd/timeet/common/domain/BaseEntity.java index bf0f9ed..b68a002 100644 --- a/src/main/java/org/dnd/modutimer/common/domain/BaseEntity.java +++ b/src/main/java/org/dnd/timeet/common/domain/BaseEntity.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.domain; +package org.dnd.timeet.common.domain; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; diff --git a/src/main/java/org/dnd/modutimer/common/exception/ApiException.java b/src/main/java/org/dnd/timeet/common/exception/ApiException.java similarity index 90% rename from src/main/java/org/dnd/modutimer/common/exception/ApiException.java rename to src/main/java/org/dnd/timeet/common/exception/ApiException.java index c0fe88b..70d7e50 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/ApiException.java +++ b/src/main/java/org/dnd/timeet/common/exception/ApiException.java @@ -1,7 +1,7 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; -import org.dnd.modutimer.common.utils.ApiUtils; +import org.dnd.timeet.common.utils.ApiUtils; import org.springframework.http.HttpStatus; import java.util.Map; diff --git a/src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java b/src/main/java/org/dnd/timeet/common/exception/BadGatewayError.java similarity index 95% rename from src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java rename to src/main/java/org/dnd/timeet/common/exception/BadGatewayError.java index dee874f..4f00f0d 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/BadGatewayError.java +++ b/src/main/java/org/dnd/timeet/common/exception/BadGatewayError.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java b/src/main/java/org/dnd/timeet/common/exception/BadRequestError.java similarity index 96% rename from src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java rename to src/main/java/org/dnd/timeet/common/exception/BadRequestError.java index 8b0f773..1bfbc16 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/BadRequestError.java +++ b/src/main/java/org/dnd/timeet/common/exception/BadRequestError.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java b/src/main/java/org/dnd/timeet/common/exception/CustomErrorAttributes.java similarity index 97% rename from src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java rename to src/main/java/org/dnd/timeet/common/exception/CustomErrorAttributes.java index 11fd56c..82ab896 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/CustomErrorAttributes.java +++ b/src/main/java/org/dnd/timeet/common/exception/CustomErrorAttributes.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; diff --git a/src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java b/src/main/java/org/dnd/timeet/common/exception/ForbiddenError.java similarity index 95% rename from src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java rename to src/main/java/org/dnd/timeet/common/exception/ForbiddenError.java index 67f3e71..db8a3a4 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/ForbiddenError.java +++ b/src/main/java/org/dnd/timeet/common/exception/ForbiddenError.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; diff --git a/src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java b/src/main/java/org/dnd/timeet/common/exception/GlobalValidationHandler.java similarity index 97% rename from src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java rename to src/main/java/org/dnd/timeet/common/exception/GlobalValidationHandler.java index 33f0e4b..700ddda 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/GlobalValidationHandler.java +++ b/src/main/java/org/dnd/timeet/common/exception/GlobalValidationHandler.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; diff --git a/src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java b/src/main/java/org/dnd/timeet/common/exception/InternalServerError.java similarity index 95% rename from src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java rename to src/main/java/org/dnd/timeet/common/exception/InternalServerError.java index f663487..c91cd51 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/InternalServerError.java +++ b/src/main/java/org/dnd/timeet/common/exception/InternalServerError.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; diff --git a/src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java b/src/main/java/org/dnd/timeet/common/exception/NotFoundError.java similarity index 95% rename from src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java rename to src/main/java/org/dnd/timeet/common/exception/NotFoundError.java index 464cd24..432a132 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/NotFoundError.java +++ b/src/main/java/org/dnd/timeet/common/exception/NotFoundError.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; diff --git a/src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java b/src/main/java/org/dnd/timeet/common/exception/UnAuthorizedError.java similarity index 95% rename from src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java rename to src/main/java/org/dnd/timeet/common/exception/UnAuthorizedError.java index 10670c9..c9c8d59 100644 --- a/src/main/java/org/dnd/modutimer/common/exception/UnAuthorizedError.java +++ b/src/main/java/org/dnd/timeet/common/exception/UnAuthorizedError.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.exception; +package org.dnd.timeet.common.exception; import lombok.Getter; diff --git a/src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java b/src/main/java/org/dnd/timeet/common/security/CustomAuthenticationEntryPoint.java similarity index 91% rename from src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java rename to src/main/java/org/dnd/timeet/common/security/CustomAuthenticationEntryPoint.java index 1309024..35f692d 100644 --- a/src/main/java/org/dnd/modutimer/common/security/CustomAuthenticationEntryPoint.java +++ b/src/main/java/org/dnd/timeet/common/security/CustomAuthenticationEntryPoint.java @@ -1,9 +1,9 @@ -package org.dnd.modutimer.common.security; +package org.dnd.timeet.common.security; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.dnd.modutimer.common.exception.UnAuthorizedError; +import org.dnd.timeet.common.exception.UnAuthorizedError; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; diff --git a/src/main/java/org/dnd/modutimer/common/security/CustomUserDetails.java b/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java similarity index 93% rename from src/main/java/org/dnd/modutimer/common/security/CustomUserDetails.java rename to src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java index bdbe8f0..392513a 100644 --- a/src/main/java/org/dnd/modutimer/common/security/CustomUserDetails.java +++ b/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java @@ -1,9 +1,9 @@ -package org.dnd.modutimer.common.security; +package org.dnd.timeet.common.security; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.dnd.modutimer.user.domain.User; +import org.dnd.timeet.user.domain.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; diff --git a/src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java b/src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java similarity index 89% rename from src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java rename to src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java index 61a0f45..fb5a574 100644 --- a/src/main/java/org/dnd/modutimer/common/security/CustomUserDetailsService.java +++ b/src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java @@ -1,9 +1,9 @@ -package org.dnd.modutimer.common.security; +package org.dnd.timeet.common.security; import lombok.RequiredArgsConstructor; -import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.user.domain.UserRepository; +import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.user.domain.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; diff --git a/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java b/src/main/java/org/dnd/timeet/common/security/JWTProvider.java similarity index 93% rename from src/main/java/org/dnd/modutimer/common/security/JWTProvider.java rename to src/main/java/org/dnd/timeet/common/security/JWTProvider.java index 0f703af..a59e3bd 100644 --- a/src/main/java/org/dnd/modutimer/common/security/JWTProvider.java +++ b/src/main/java/org/dnd/timeet/common/security/JWTProvider.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.security; +package org.dnd.timeet.common.security; import com.auth0.jwt.JWT; @@ -6,7 +6,7 @@ import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; -import org.dnd.modutimer.user.domain.User; +import org.dnd.timeet.user.domain.User; import org.springframework.stereotype.Component; import java.util.Date; diff --git a/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java b/src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java similarity index 87% rename from src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java rename to src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java index 39208a0..3e34f7b 100644 --- a/src/main/java/org/dnd/modutimer/common/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.security; +package org.dnd.timeet.common.security; import com.auth0.jwt.exceptions.SignatureVerificationException; @@ -9,13 +9,14 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.dnd.modutimer.common.exception.ApiException; -import org.dnd.modutimer.common.exception.BadRequestError; -import org.dnd.modutimer.common.exception.InternalServerError; -import org.dnd.modutimer.common.exception.UnAuthorizedError; -import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.common.utils.ApiUtils; -import org.dnd.modutimer.user.application.UserFindService; +import org.dnd.timeet.common.exception.ApiException; +import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.common.exception.InternalServerError; +import org.dnd.timeet.common.exception.UnAuthorizedError; +import org.dnd.timeet.config.SecurityConfig; +import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.common.utils.ApiUtils; +import org.dnd.timeet.user.application.UserFindService; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -26,8 +27,6 @@ import java.io.IOException; import java.util.Collections; -import static org.dnd.modutimer.config.SecurityConfig.PUBLIC_URLS; - @Slf4j public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @@ -78,7 +77,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 인증이 필요하지 않는 url private boolean isNonProtectedUrl(HttpServletRequest request) { - for (String urlPattern : PUBLIC_URLS) { + for (String urlPattern : SecurityConfig.PUBLIC_URLS) { AntPathRequestMatcher matcher = new AntPathRequestMatcher(urlPattern); if (matcher.matches(request)) { return true; diff --git a/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java b/src/main/java/org/dnd/timeet/common/utils/ApiUtils.java similarity index 97% rename from src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java rename to src/main/java/org/dnd/timeet/common/utils/ApiUtils.java index 12b5b07..038a1e6 100644 --- a/src/main/java/org/dnd/modutimer/common/utils/ApiUtils.java +++ b/src/main/java/org/dnd/timeet/common/utils/ApiUtils.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.common.utils; +package org.dnd.timeet.common.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java b/src/main/java/org/dnd/timeet/common/utils/FilterResponseUtils.java similarity index 91% rename from src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java rename to src/main/java/org/dnd/timeet/common/utils/FilterResponseUtils.java index f0f616b..5cc521f 100644 --- a/src/main/java/org/dnd/modutimer/common/utils/FilterResponseUtils.java +++ b/src/main/java/org/dnd/timeet/common/utils/FilterResponseUtils.java @@ -1,10 +1,10 @@ -package org.dnd.modutimer.common.utils; +package org.dnd.timeet.common.utils; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; -import org.dnd.modutimer.common.exception.ForbiddenError; -import org.dnd.modutimer.common.exception.UnAuthorizedError; +import org.dnd.timeet.common.exception.ForbiddenError; +import org.dnd.timeet.common.exception.UnAuthorizedError; import org.springframework.http.HttpStatus; import java.io.IOException; diff --git a/src/main/java/org/dnd/modutimer/config/SecurityConfig.java b/src/main/java/org/dnd/timeet/config/SecurityConfig.java similarity index 95% rename from src/main/java/org/dnd/modutimer/config/SecurityConfig.java rename to src/main/java/org/dnd/timeet/config/SecurityConfig.java index c34b646..e63f374 100644 --- a/src/main/java/org/dnd/modutimer/config/SecurityConfig.java +++ b/src/main/java/org/dnd/timeet/config/SecurityConfig.java @@ -1,12 +1,12 @@ -package org.dnd.modutimer.config; +package org.dnd.timeet.config; import java.util.Collections; -import org.dnd.modutimer.common.exception.ForbiddenError; -import org.dnd.modutimer.common.security.CustomAuthenticationEntryPoint; -import org.dnd.modutimer.common.security.JwtAuthenticationFilter; -import org.dnd.modutimer.user.application.UserFindService; -import org.dnd.modutimer.common.utils.FilterResponseUtils; +import org.dnd.timeet.common.exception.ForbiddenError; +import org.dnd.timeet.common.security.CustomAuthenticationEntryPoint; +import org.dnd.timeet.common.security.JwtAuthenticationFilter; +import org.dnd.timeet.user.application.UserFindService; +import org.dnd.timeet.common.utils.FilterResponseUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/org/dnd/modutimer/config/SwaggerConfig.java b/src/main/java/org/dnd/timeet/config/SwaggerConfig.java similarity index 91% rename from src/main/java/org/dnd/modutimer/config/SwaggerConfig.java rename to src/main/java/org/dnd/timeet/config/SwaggerConfig.java index 13d6907..8117bd5 100644 --- a/src/main/java/org/dnd/modutimer/config/SwaggerConfig.java +++ b/src/main/java/org/dnd/timeet/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.config; +package org.dnd.timeet.config; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.models.Components; @@ -10,7 +10,7 @@ import org.springframework.context.annotation.Configuration; @OpenAPIDefinition( - info = @io.swagger.v3.oas.annotations.info.Info(title = "ModuTimer API 명세서", + info = @io.swagger.v3.oas.annotations.info.Info(title = "Timeet API 명세서", description = "모두의 타이머 API 명세서", version = "v1")) @Configuration diff --git a/src/main/java/org/dnd/modutimer/timer/application/TimerService.java b/src/main/java/org/dnd/timeet/timer/application/TimerService.java similarity index 86% rename from src/main/java/org/dnd/modutimer/timer/application/TimerService.java rename to src/main/java/org/dnd/timeet/timer/application/TimerService.java index f2ee668..33140f1 100644 --- a/src/main/java/org/dnd/modutimer/timer/application/TimerService.java +++ b/src/main/java/org/dnd/timeet/timer/application/TimerService.java @@ -1,13 +1,13 @@ -package org.dnd.modutimer.timer.application; +package org.dnd.timeet.timer.application; import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; -import org.dnd.modutimer.common.exception.NotFoundError; -import org.dnd.modutimer.timer.domain.Duration; -import org.dnd.modutimer.timer.domain.Timer; -import org.dnd.modutimer.timer.domain.TimerRepository; -import org.dnd.modutimer.timer.dto.TimerCreateRequest; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.timer.domain.Duration; +import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.timer.domain.TimerRepository; +import org.dnd.timeet.timer.dto.TimerCreateRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/org/dnd/modutimer/timer/controller/TimerController.java b/src/main/java/org/dnd/timeet/timer/controller/TimerController.java similarity index 92% rename from src/main/java/org/dnd/modutimer/timer/controller/TimerController.java rename to src/main/java/org/dnd/timeet/timer/controller/TimerController.java index 58329f2..6351cff 100644 --- a/src/main/java/org/dnd/modutimer/timer/controller/TimerController.java +++ b/src/main/java/org/dnd/timeet/timer/controller/TimerController.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.timer.controller; +package org.dnd.timeet.timer.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -7,10 +7,10 @@ import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.dnd.modutimer.timer.application.TimerService; -import org.dnd.modutimer.timer.domain.Timer; -import org.dnd.modutimer.timer.dto.TimerCreateRequest; -import org.dnd.modutimer.timer.dto.TimerInfoResponse; +import org.dnd.timeet.timer.application.TimerService; +import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.timer.dto.TimerCreateRequest; +import org.dnd.timeet.timer.dto.TimerInfoResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/org/dnd/modutimer/timer/domain/Duration.java b/src/main/java/org/dnd/timeet/timer/domain/Duration.java similarity index 97% rename from src/main/java/org/dnd/modutimer/timer/domain/Duration.java rename to src/main/java/org/dnd/timeet/timer/domain/Duration.java index f6351ed..08168c4 100644 --- a/src/main/java/org/dnd/modutimer/timer/domain/Duration.java +++ b/src/main/java/org/dnd/timeet/timer/domain/Duration.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.timer.domain; +package org.dnd.timeet.timer.domain; import jakarta.persistence.Embeddable; import lombok.AccessLevel; diff --git a/src/main/java/org/dnd/modutimer/timer/domain/Timer.java b/src/main/java/org/dnd/timeet/timer/domain/Timer.java similarity index 95% rename from src/main/java/org/dnd/modutimer/timer/domain/Timer.java rename to src/main/java/org/dnd/timeet/timer/domain/Timer.java index 95d3ec1..10dce21 100644 --- a/src/main/java/org/dnd/modutimer/timer/domain/Timer.java +++ b/src/main/java/org/dnd/timeet/timer/domain/Timer.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.timer.domain; +package org.dnd.timeet.timer.domain; import jakarta.persistence.AttributeOverride; @@ -12,7 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.dnd.modutimer.common.domain.AuditableEntity; +import org.dnd.timeet.common.domain.AuditableEntity; import org.hibernate.annotations.Where; @Entity diff --git a/src/main/java/org/dnd/modutimer/timer/domain/TimerRepository.java b/src/main/java/org/dnd/timeet/timer/domain/TimerRepository.java similarity index 81% rename from src/main/java/org/dnd/modutimer/timer/domain/TimerRepository.java rename to src/main/java/org/dnd/timeet/timer/domain/TimerRepository.java index ad5414f..d2ba1bd 100644 --- a/src/main/java/org/dnd/modutimer/timer/domain/TimerRepository.java +++ b/src/main/java/org/dnd/timeet/timer/domain/TimerRepository.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.timer.domain; +package org.dnd.timeet.timer.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/org/dnd/modutimer/timer/domain/TimerStatus.java b/src/main/java/org/dnd/timeet/timer/domain/TimerStatus.java similarity index 55% rename from src/main/java/org/dnd/modutimer/timer/domain/TimerStatus.java rename to src/main/java/org/dnd/timeet/timer/domain/TimerStatus.java index 896d8bf..765bdb9 100644 --- a/src/main/java/org/dnd/modutimer/timer/domain/TimerStatus.java +++ b/src/main/java/org/dnd/timeet/timer/domain/TimerStatus.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.timer.domain; +package org.dnd.timeet.timer.domain; public enum TimerStatus { RUNNING, STOPPED diff --git a/src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java b/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java similarity index 88% rename from src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java rename to src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java index 315533e..c897c91 100644 --- a/src/main/java/org/dnd/modutimer/timer/dto/TimerCreateRequest.java +++ b/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java @@ -1,13 +1,13 @@ -package org.dnd.modutimer.timer.dto; +package org.dnd.timeet.timer.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.dnd.modutimer.timer.domain.Duration; -import org.dnd.modutimer.timer.domain.Timer; -import org.dnd.modutimer.timer.domain.TimerStatus; +import org.dnd.timeet.timer.domain.Duration; +import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.timer.domain.TimerStatus; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; diff --git a/src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java b/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java similarity index 90% rename from src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java rename to src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java index db911dd..3fbdd19 100644 --- a/src/main/java/org/dnd/modutimer/timer/dto/TimerInfoResponse.java +++ b/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java @@ -1,11 +1,11 @@ -package org.dnd.modutimer.timer.dto; +package org.dnd.timeet.timer.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; import lombok.Setter; -import org.dnd.modutimer.timer.domain.Timer; -import org.dnd.modutimer.timer.domain.TimerStatus; +import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.timer.domain.TimerStatus; @Getter @Setter diff --git a/src/main/java/org/dnd/modutimer/user/application/UserFindService.java b/src/main/java/org/dnd/timeet/user/application/UserFindService.java similarity index 76% rename from src/main/java/org/dnd/modutimer/user/application/UserFindService.java rename to src/main/java/org/dnd/timeet/user/application/UserFindService.java index 18571ad..4269e7a 100644 --- a/src/main/java/org/dnd/modutimer/user/application/UserFindService.java +++ b/src/main/java/org/dnd/timeet/user/application/UserFindService.java @@ -1,8 +1,8 @@ -package org.dnd.modutimer.user.application; +package org.dnd.timeet.user.application; -import org.dnd.modutimer.common.exception.NotFoundError; -import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.user.domain.UserRepository; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.user.domain.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/dnd/modutimer/user/application/UserService.java b/src/main/java/org/dnd/timeet/user/application/UserService.java similarity index 81% rename from src/main/java/org/dnd/modutimer/user/application/UserService.java rename to src/main/java/org/dnd/timeet/user/application/UserService.java index 60c1348..a8e352a 100644 --- a/src/main/java/org/dnd/modutimer/user/application/UserService.java +++ b/src/main/java/org/dnd/timeet/user/application/UserService.java @@ -1,17 +1,17 @@ -package org.dnd.modutimer.user.application; +package org.dnd.timeet.user.application; import lombok.RequiredArgsConstructor; -import org.dnd.modutimer.common.exception.BadRequestError; -import org.dnd.modutimer.common.exception.InternalServerError; -import org.dnd.modutimer.common.exception.NotFoundError; -import org.dnd.modutimer.common.exception.UnAuthorizedError; -import org.dnd.modutimer.common.security.JWTProvider; -import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.user.domain.UserRepository; -import org.dnd.modutimer.user.dto.UserLoginRequest; -import org.dnd.modutimer.user.dto.UserLoginResponse; -import org.dnd.modutimer.user.dto.UserRegisterRequest; +import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.common.exception.InternalServerError; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.common.exception.UnAuthorizedError; +import org.dnd.timeet.common.security.JWTProvider; +import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.user.domain.UserRepository; +import org.dnd.timeet.user.dto.UserLoginRequest; +import org.dnd.timeet.user.dto.UserLoginResponse; +import org.dnd.timeet.user.dto.UserRegisterRequest; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/org/dnd/modutimer/user/controller/UserController.java b/src/main/java/org/dnd/timeet/user/controller/UserController.java similarity index 84% rename from src/main/java/org/dnd/modutimer/user/controller/UserController.java rename to src/main/java/org/dnd/timeet/user/controller/UserController.java index 866771e..846491b 100644 --- a/src/main/java/org/dnd/modutimer/user/controller/UserController.java +++ b/src/main/java/org/dnd/timeet/user/controller/UserController.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.controller; +package org.dnd.timeet.user.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -8,15 +8,15 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.dnd.modutimer.common.exception.BadRequestError; -import org.dnd.modutimer.common.exception.InternalServerError; -import org.dnd.modutimer.common.security.JWTProvider; -import org.dnd.modutimer.user.application.UserService; -import org.dnd.modutimer.user.dto.EmailCheckRequest; -import org.dnd.modutimer.user.dto.UserLoginRequest; -import org.dnd.modutimer.user.dto.UserLoginResponse; -import org.dnd.modutimer.user.dto.UserRegisterRequest; -import org.dnd.modutimer.common.utils.ApiUtils; +import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.common.exception.InternalServerError; +import org.dnd.timeet.common.security.JWTProvider; +import org.dnd.timeet.user.application.UserService; +import org.dnd.timeet.user.dto.EmailCheckRequest; +import org.dnd.timeet.user.dto.UserLoginRequest; +import org.dnd.timeet.user.dto.UserLoginResponse; +import org.dnd.timeet.user.dto.UserRegisterRequest; +import org.dnd.timeet.common.utils.ApiUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; diff --git a/src/main/java/org/dnd/modutimer/user/domain/User.java b/src/main/java/org/dnd/timeet/user/domain/User.java similarity index 93% rename from src/main/java/org/dnd/modutimer/user/domain/User.java rename to src/main/java/org/dnd/timeet/user/domain/User.java index efbcbe7..9c0e856 100644 --- a/src/main/java/org/dnd/modutimer/user/domain/User.java +++ b/src/main/java/org/dnd/timeet/user/domain/User.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.domain; +package org.dnd.timeet.user.domain; import jakarta.persistence.AttributeOverride; import jakarta.persistence.Column; @@ -10,7 +10,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.dnd.modutimer.common.domain.BaseEntity; +import org.dnd.timeet.common.domain.BaseEntity; import org.hibernate.annotations.Where; @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/org/dnd/modutimer/user/domain/UserRepository.java b/src/main/java/org/dnd/timeet/user/domain/UserRepository.java similarity index 84% rename from src/main/java/org/dnd/modutimer/user/domain/UserRepository.java rename to src/main/java/org/dnd/timeet/user/domain/UserRepository.java index 3a0d263..3e9a949 100644 --- a/src/main/java/org/dnd/modutimer/user/domain/UserRepository.java +++ b/src/main/java/org/dnd/timeet/user/domain/UserRepository.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.domain; +package org.dnd.timeet.user.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/org/dnd/modutimer/user/domain/UserRole.java b/src/main/java/org/dnd/timeet/user/domain/UserRole.java similarity index 61% rename from src/main/java/org/dnd/modutimer/user/domain/UserRole.java rename to src/main/java/org/dnd/timeet/user/domain/UserRole.java index 5a8a121..610dc42 100644 --- a/src/main/java/org/dnd/modutimer/user/domain/UserRole.java +++ b/src/main/java/org/dnd/timeet/user/domain/UserRole.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.domain; +package org.dnd.timeet.user.domain; public enum UserRole { ROLE_USER, ROLE_OWNER, ROLE_ADMIN diff --git a/src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java b/src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java similarity index 93% rename from src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java rename to src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java index d3cab5a..7a412df 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/EmailCheckRequest.java +++ b/src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.dto; +package org.dnd.timeet.user.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java b/src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java similarity index 95% rename from src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java rename to src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java index f455cc1..5e1136d 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UpdatePasswordRequest.java +++ b/src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.dto; +package org.dnd.timeet.user.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java b/src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java similarity index 88% rename from src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java rename to src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java index 9d01f94..6ff1e2f 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserInfoResponse.java +++ b/src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java @@ -1,10 +1,10 @@ -package org.dnd.modutimer.user.dto; +package org.dnd.timeet.user.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; -import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.user.domain.UserRole; +import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.user.domain.UserRole; @Getter @Setter diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java b/src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java similarity index 96% rename from src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java rename to src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java index db5872d..32d0b3c 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserLoginRequest.java +++ b/src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.dto; +package org.dnd.timeet.user.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java b/src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java similarity index 89% rename from src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java rename to src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java index 6867b55..917c7b1 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserLoginResponse.java +++ b/src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.dto; +package org.dnd.timeet.user.dto; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java b/src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java similarity index 92% rename from src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java rename to src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java index a948a64..d489b08 100644 --- a/src/main/java/org/dnd/modutimer/user/dto/UserRegisterRequest.java +++ b/src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer.user.dto; +package org.dnd.timeet.user.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -6,8 +6,8 @@ import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; -import org.dnd.modutimer.user.domain.User; -import org.dnd.modutimer.user.domain.UserRole; +import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.user.domain.UserRole; @Getter @Setter diff --git a/src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java b/src/test/java/org/dnd/timeet/TimeetApplicationTests.java similarity index 83% rename from src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java rename to src/test/java/org/dnd/timeet/TimeetApplicationTests.java index 598a922..eaa3d23 100644 --- a/src/test/java/org/dnd/modutimer/ModutimerApplicationTests.java +++ b/src/test/java/org/dnd/timeet/TimeetApplicationTests.java @@ -1,4 +1,4 @@ -package org.dnd.modutimer; +package org.dnd.timeet; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -6,7 +6,7 @@ @SpringBootTest @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) -class ModutimerApplicationTests { +class TimeetApplicationTests { @Test void contextLoads() { From 8d6556709586817104ce20dea1c86c67e9f3c0e5 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Tue, 6 Feb 2024 14:05:47 +0900 Subject: [PATCH 12/81] [#15] refactor: Eliminate unnecessary imports --- .../java/org/dnd/timeet/common/domain/AuditableEntity.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java b/src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java index d55f4fa..0b29e3b 100644 --- a/src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java +++ b/src/main/java/org/dnd/timeet/common/domain/AuditableEntity.java @@ -1,7 +1,8 @@ package org.dnd.timeet.common.domain; -import jakarta.persistence.*; -import lombok.EqualsAndHashCode; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; import lombok.Getter; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; From 13d9380c37e8701792845a161ccd45d00ca03e0c Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Tue, 6 Feb 2024 14:48:10 +0900 Subject: [PATCH 13/81] [#15] refactor: Integrate Checkstyle in build.yml, exclude Javadox checks --- .../pull_request_template.md | 0 .github/workflows/build.yml | 2 + build.gradle | 6 + config/checkstyle-config.xml | 382 +++++++++++ config/formatter-config.xml | 598 ++++++++++++++++++ 5 files changed, 988 insertions(+) rename .github/{ => PR_TEMPLATE}/pull_request_template.md (100%) create mode 100644 config/checkstyle-config.xml create mode 100644 config/formatter-config.xml diff --git a/.github/pull_request_template.md b/.github/PR_TEMPLATE/pull_request_template.md similarity index 100% rename from .github/pull_request_template.md rename to .github/PR_TEMPLATE/pull_request_template.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf02042..dcdb8a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,6 +15,8 @@ jobs: uses: actions/setup-java@v1 with: java-version: 17 + - name: Run Checkstyle + run: ./gradlew checkstyleMain checkstyleTest - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml diff --git a/build.gradle b/build.gradle index 4b9a275..c6a4cd9 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.springframework.boot' version '3.2.1' id 'io.spring.dependency-management' version '1.1.4' id "org.sonarqube" version "4.0.0.2929" + id 'checkstyle' } group = 'org.dnd' @@ -56,4 +57,9 @@ sonar { properties { property "sonar.projectKey", "dnd-10th-2-backend" } +} + +checkstyle { + toolVersion '8.45' + configFile file("${project.rootDir}/config/checkstyle-config.xml") } \ No newline at end of file diff --git a/config/checkstyle-config.xml b/config/checkstyle-config.xml new file mode 100644 index 0000000..cccde2e --- /dev/null +++ b/config/checkstyle-config.xml @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/formatter-config.xml b/config/formatter-config.xml new file mode 100644 index 0000000..98f81e9 --- /dev/null +++ b/config/formatter-config.xml @@ -0,0 +1,598 @@ + + + + + + From 5ae2736529cfa82144fdd41d5813ba9d9de41895 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Tue, 6 Feb 2024 16:15:36 +0900 Subject: [PATCH 14/81] [#-] feat: Add checkstyle plugin and configuration for build process --- .github/workflows/build.yml | 4 ++-- build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcdb8a3..31dec9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,8 @@ jobs: uses: actions/setup-java@v1 with: java-version: 17 - - name: Run Checkstyle - run: ./gradlew checkstyleMain checkstyleTest +# - name: Run Checkstyle +# run: ./gradlew checkstyleMain checkstyleTest - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml diff --git a/build.gradle b/build.gradle index c6a4cd9..50c4589 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,6 @@ sonar { } checkstyle { - toolVersion '8.45' - configFile file("${project.rootDir}/config/checkstyle-config.xml") + toolVersion '8.1' + configFile = file('config/checkstyle-config.xml') // Path to your config file } \ No newline at end of file From 0f09e4a02091a580e1db337bb1a68d6584db47d4 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Tue, 6 Feb 2024 17:34:40 +0900 Subject: [PATCH 15/81] [#17] chore: temporarily disable SonarQube plugin --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 50c4589..b7bc85a 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.springframework.boot' version '3.2.1' id 'io.spring.dependency-management' version '1.1.4' id "org.sonarqube" version "4.0.0.2929" - id 'checkstyle' +// id 'checkstyle' } group = 'org.dnd' @@ -59,7 +59,7 @@ sonar { } } -checkstyle { - toolVersion '8.1' - configFile = file('config/checkstyle-config.xml') // Path to your config file -} \ No newline at end of file +//checkstyle { +// toolVersion '8.1' +// configFile = file('config/checkstyle-config.xml') // Path to your config file +//} \ No newline at end of file From 26c7c3e72879fe9c9d41a868403e697017c975e7 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Tue, 6 Feb 2024 18:00:47 +0900 Subject: [PATCH 16/81] [#7] feat: Integrate Checkstyle into Github actions workflow --- .github/workflows/build.yml | 4 ++-- build.gradle | 10 +++++----- config/checkstyle-config.xml | 12 +++++------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31dec9a..dcdb8a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,8 @@ jobs: uses: actions/setup-java@v1 with: java-version: 17 -# - name: Run Checkstyle -# run: ./gradlew checkstyleMain checkstyleTest + - name: Run Checkstyle + run: ./gradlew checkstyleMain checkstyleTest - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml diff --git a/build.gradle b/build.gradle index b7bc85a..54abd53 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org.springframework.boot' version '3.2.1' id 'io.spring.dependency-management' version '1.1.4' id "org.sonarqube" version "4.0.0.2929" -// id 'checkstyle' + id 'checkstyle' } group = 'org.dnd' @@ -59,7 +59,7 @@ sonar { } } -//checkstyle { -// toolVersion '8.1' -// configFile = file('config/checkstyle-config.xml') // Path to your config file -//} \ No newline at end of file +checkstyle { + toolVersion '10.13.0' + configFile file("${project.rootDir}/config/checkstyle-config.xml") +} \ No newline at end of file diff --git a/config/checkstyle-config.xml b/config/checkstyle-config.xml index cccde2e..8363b24 100644 --- a/config/checkstyle-config.xml +++ b/config/checkstyle-config.xml @@ -41,13 +41,6 @@ - - - - - - - @@ -379,4 +372,9 @@ + + + + + From 9808d6824c47766eb90211e14e39fab479c58cb8 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 7 Feb 2024 16:07:42 +0900 Subject: [PATCH 17/81] [#-] feat: Add TimerInfoResponse and @Schema annotation --- .../java/org/dnd/timeet/timer/dto/TimerCreateRequest.java | 2 ++ src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java b/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java index c897c91..7a94358 100644 --- a/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java +++ b/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java @@ -13,6 +13,8 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; + +@Schema(description = "타이머 생성 요청") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java b/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java index 3fbdd19..61ef7f2 100644 --- a/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java +++ b/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java @@ -7,6 +7,7 @@ import org.dnd.timeet.timer.domain.Timer; import org.dnd.timeet.timer.domain.TimerStatus; +@Schema(description = "타이머 정보 응답") @Getter @Setter public class TimerInfoResponse { @@ -41,5 +42,4 @@ public static TimerInfoResponse from(Timer timer) { // 매개변수로부터 객 .endTime(timer.getDuration().getEndTime()) .build(); } -} - +} \ No newline at end of file From ec1f4d5659ef31ca5a5f2926d4492d438b9da124 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 7 Feb 2024 19:16:34 +0900 Subject: [PATCH 18/81] [#19] feat: Develop meeting management API for creating and retrieving by id; link generation not included yet --- .../org/dnd/timeet/config/SecurityConfig.java | 3 +- .../meeting/application/MeetingService.java | 37 +++++++ .../meeting/controller/MeetingController.java | 50 ++++++++++ .../dnd/timeet/meeting/domain/Meeting.java | 96 +++++++++++++++++++ .../meeting/domain/MeetingRepository.java | 8 ++ .../timeet/meeting/domain/MeetingStatus.java | 5 + .../meeting/dto/MeetingCreateRequest.java | 51 ++++++++++ .../meeting/dto/MeetingCreateResponse.java | 37 +++++++ .../meeting/dto/MeetingInfoResponse.java | 40 ++++++++ .../timer/application/TimerService.java | 6 +- .../timer/controller/TimerController.java | 1 + .../org/dnd/timeet/timer/domain/Timer.java | 14 +-- .../{Duration.java => TimerDuration.java} | 10 +- .../timeet/timer/dto/TimerCreateRequest.java | 4 +- .../timeet/timer/dto/TimerInfoResponse.java | 4 +- 15 files changed, 346 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/meeting/application/MeetingService.java create mode 100644 src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java create mode 100644 src/main/java/org/dnd/timeet/meeting/domain/Meeting.java create mode 100644 src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java create mode 100644 src/main/java/org/dnd/timeet/meeting/domain/MeetingStatus.java create mode 100644 src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java create mode 100644 src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateResponse.java create mode 100644 src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java rename src/main/java/org/dnd/timeet/timer/domain/{Duration.java => TimerDuration.java} (78%) diff --git a/src/main/java/org/dnd/timeet/config/SecurityConfig.java b/src/main/java/org/dnd/timeet/config/SecurityConfig.java index e63f374..2321d39 100644 --- a/src/main/java/org/dnd/timeet/config/SecurityConfig.java +++ b/src/main/java/org/dnd/timeet/config/SecurityConfig.java @@ -50,7 +50,8 @@ public class SecurityConfig { // open url "/api/v1/users/**", //h2-console - "/h2-console/**" + "/h2-console/**", + "/api/meetings/**", }; diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java new file mode 100644 index 0000000..d076203 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -0,0 +1,37 @@ +package org.dnd.timeet.meeting.application; + +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.NotFoundError; + +import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.meeting.domain.MeetingRepository; + +import org.dnd.timeet.meeting.dto.MeetingCreateRequest; +import org.dnd.timeet.timer.domain.Timer; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor // final 의존성 주입 +@Transactional // DB 변경 작업에 사용 +public class MeetingService { + + private final MeetingRepository meetingRepository; + + public Meeting createMeeting(MeetingCreateRequest createDto) { + Meeting meeting = createDto.toEntity(); + // 복잡한 비즈니스 로직은 도메인 메서드를 이용하여 Service에서 처리 + return meetingRepository.save(meeting); + } + + @Transactional(readOnly = true) + public Meeting findById(Long id) { + return meetingRepository.findById(id) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + } + + +} diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java new file mode 100644 index 0000000..0a31805 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -0,0 +1,50 @@ +package org.dnd.timeet.meeting.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.utils.ApiUtils; +import org.dnd.timeet.meeting.application.MeetingService; +import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.meeting.dto.MeetingCreateRequest; +import org.dnd.timeet.meeting.dto.MeetingCreateResponse; +import org.dnd.timeet.meeting.dto.MeetingInfoResponse; +import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.timer.dto.TimerInfoResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "회의 컨트롤러", description = "Meeting API입니다.") +@RestController +@RequestMapping("/api/meetings") +@RequiredArgsConstructor +public class MeetingController { + + private final MeetingService meetingService; + + @PostMapping + @Operation(summary = "회의 생성", description = "회의를 생성한다.") + public ResponseEntity createMeeting(@RequestBody @Valid MeetingCreateRequest meetingCreateRequest) { + // TODO : 유저 인증 로직 추가 + Meeting savedMeeting = meetingService.createMeeting(meetingCreateRequest); + MeetingCreateResponse meetingCreateResponse = MeetingCreateResponse.from(savedMeeting); + + return ResponseEntity.ok(ApiUtils.success(meetingCreateResponse)); + } + + @GetMapping("/{id}") + @Operation(summary = "단일 회의 조회", description = "지정된 id에 해당하는 회의를 조회한다.") + public ResponseEntity getTimerById(@PathVariable("id") Long meetingId) { + Meeting meeting = meetingService.findById(meetingId); + MeetingInfoResponse meetingInfoResponse = MeetingInfoResponse.from(meeting); + + return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); + } + +} diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java new file mode 100644 index 0000000..39317bd --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -0,0 +1,96 @@ +package org.dnd.timeet.meeting.domain; + + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.dnd.timeet.common.domain.AuditableEntity; +import org.dnd.timeet.common.exception.BadRequestError; +import org.hibernate.annotations.Where; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "meeting") +@AttributeOverride(name = "id", column = @Column(name = "meeting_id")) +@Where(clause = "is_deleted=false") +public class Meeting extends AuditableEntity { + + @Column(nullable = false, length = 255) + private String title; + + @Column(nullable = false) + private LocalDateTime startTime; + + // 종료시간, 초기값 null + private LocalDateTime endTime; + + // 예상 소요 시간 + @Column(nullable = false) + private LocalTime totalEstimatedDuration; + + // 실제 소요 시간, 초기값 null + private LocalTime totalActualDuration; + + @Column(nullable = false, length = 255) + private String location; + + @Column(nullable = true, length = 1000) + private String description; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private MeetingStatus status = MeetingStatus.SCHEDULED; + + @Builder + public Meeting(String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, String location, + String description) { + this.title = title; + this.startTime = startTime; + this.totalEstimatedDuration = totalEstimatedDuration; + this.location = location; + this.description = description; // 입력한 값이 없을 경우 null + } + + + // 회의를 시작하는 메서드 + public void startMeeting() { + this.status = MeetingStatus.INPROGRESS; + } + + // 회의를 종료하는 메서드 + public void endMeeting() { + this.endTime = LocalDateTime.now(); + this.status = MeetingStatus.COMPLETED; + + long durationInSeconds = Duration.between(startTime, endTime).getSeconds(); + this.totalActualDuration = LocalTime.ofSecondOfDay(durationInSeconds); + } + + // 회의를 취소하는 메서드 + public void cancelMeeting() { + this.status = MeetingStatus.CANCELED; + this.delete(); + } + + // 회의 시작 시간 수정하는 메서드 + public void updateStartTime(LocalDateTime startTime) { + if (this.status != MeetingStatus.SCHEDULED) { + throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("Meeting", "MeetingStatus is not SCHEDULED")); + } + this.startTime = startTime; + } +} + diff --git a/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java b/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java new file mode 100644 index 0000000..f0f9304 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java @@ -0,0 +1,8 @@ +package org.dnd.timeet.meeting.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MeetingRepository extends JpaRepository { + +// Optional findByUserId(Long id); +} diff --git a/src/main/java/org/dnd/timeet/meeting/domain/MeetingStatus.java b/src/main/java/org/dnd/timeet/meeting/domain/MeetingStatus.java new file mode 100644 index 0000000..2e13f95 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/domain/MeetingStatus.java @@ -0,0 +1,5 @@ +package org.dnd.timeet.meeting.domain; + +public enum MeetingStatus { + SCHEDULED, INPROGRESS, COMPLETED, CANCELED +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java new file mode 100644 index 0000000..ea88276 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java @@ -0,0 +1,51 @@ +package org.dnd.timeet.meeting.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.dnd.timeet.meeting.domain.Meeting; +import org.springframework.format.annotation.DateTimeFormat; + + +@Schema(description = "회의 생성 요청") +@Getter +@Setter +@NoArgsConstructor +public class MeetingCreateRequest { + + @NotNull(message = "회의 제목은 반드시 입력되어야 합니다") + @Schema(description = "회의 제목", example = "2차 업무 회의") + private String title; + + @NotNull(message = "회의 장소는 반드시 입력되어야 합니다") + @Schema(description = "회의 장소", example = "스타벅스 강남역점") + private String location; + + @NotNull(message = "회의 시작 시간은 반드시 입력되어야 합니다") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + @Schema(description = "회의 시작 시간", example = "2024-01-11T13:20") + private LocalDateTime startTime; + + @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") + private String description; + + @NotNull(message = "예상 소요 시간은 반드시 입력되어야 합니다") + @DateTimeFormat(pattern = "HH:mm") + @Schema(description = "예상 소요 시간", example = "02:00") + private LocalTime estimatedTotalDuration; + + public Meeting toEntity() { + return Meeting.builder() + .title(this.title) + .location(this.location) + .startTime(startTime) + .description(this.description) + .totalEstimatedDuration(this.estimatedTotalDuration) + .build(); + } +} diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateResponse.java new file mode 100644 index 0000000..22127af --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateResponse.java @@ -0,0 +1,37 @@ +package org.dnd.timeet.meeting.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.dnd.timeet.meeting.domain.Meeting; + + +@Schema(description = "회의 생성 응답") +@Getter +@Setter +@NoArgsConstructor +public class MeetingCreateResponse { + + @Schema(description = "회의 id", example = "12L") + private Long meetingId; + +// @Schema(description = "회의 공유 url", example = "http://localhost:8080/meetings/12L") +// private String shareUrl; + + @Builder +// public MeetingCreateResponse(Long meetingId, String shareUrl) { + public MeetingCreateResponse(Long meetingId) { + this.meetingId = meetingId; +// this.shareUrl = shareUrl; + } + + // 매개변수로부터 객체를 생성하는 팩토리 메서드 + public static MeetingCreateResponse from(Meeting meeting) { + return MeetingCreateResponse.builder() + .meetingId(meeting.getId()) +// .shareUrl(link.getUri().toString()) + .build(); + } +} diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java new file mode 100644 index 0000000..11b2730 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java @@ -0,0 +1,40 @@ +package org.dnd.timeet.meeting.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.timer.domain.TimerStatus; + +@Schema(description = "회의 정보 응답") +@Getter +@Setter +public class MeetingInfoResponse { + + @Schema(description = "회의 id", example = "12L") + private Long meetingId; + + @Schema(description = "회의 제목", example = "2차 회의") + private String title; + + @Schema(description = "회의 목", example = "2개의 사안 모두 해결하기") + private String description; + + @Builder + public MeetingInfoResponse(Long meetingId, String title, String description) { + this.meetingId = meetingId; + this.title = title; + this.description = description; + } + + + public static MeetingInfoResponse from(Meeting meeting) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 + return MeetingInfoResponse.builder() + .meetingId(meeting.getId()) + .title(meeting.getTitle()) + .description(meeting.getDescription()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/timer/application/TimerService.java b/src/main/java/org/dnd/timeet/timer/application/TimerService.java index 33140f1..4738c1c 100644 --- a/src/main/java/org/dnd/timeet/timer/application/TimerService.java +++ b/src/main/java/org/dnd/timeet/timer/application/TimerService.java @@ -4,7 +4,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.NotFoundError; -import org.dnd.timeet.timer.domain.Duration; +import org.dnd.timeet.timer.domain.TimerDuration; import org.dnd.timeet.timer.domain.Timer; import org.dnd.timeet.timer.domain.TimerRepository; import org.dnd.timeet.timer.dto.TimerCreateRequest; @@ -46,9 +46,9 @@ public void stopTimer(Long timerId) { timer.stopTimer(); } - public void changeDuration(Long timerId, Duration newDuration) { + public void changeDuration(Long timerId, TimerDuration newTimerDuration) { Timer timer = findById(timerId); - timer.changeDuration(newDuration); + timer.changeDuration(newTimerDuration); } public void deleteTimer(Long timerId) { diff --git a/src/main/java/org/dnd/timeet/timer/controller/TimerController.java b/src/main/java/org/dnd/timeet/timer/controller/TimerController.java index 6351cff..5fcfbd8 100644 --- a/src/main/java/org/dnd/timeet/timer/controller/TimerController.java +++ b/src/main/java/org/dnd/timeet/timer/controller/TimerController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.dnd.timeet.timer.application.TimerService; import org.dnd.timeet.timer.domain.Timer; + import org.dnd.timeet.timer.dto.TimerCreateRequest; import org.dnd.timeet.timer.dto.TimerInfoResponse; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/org/dnd/timeet/timer/domain/Timer.java b/src/main/java/org/dnd/timeet/timer/domain/Timer.java index 10dce21..2f964eb 100644 --- a/src/main/java/org/dnd/timeet/timer/domain/Timer.java +++ b/src/main/java/org/dnd/timeet/timer/domain/Timer.java @@ -27,18 +27,18 @@ public class Timer extends AuditableEntity { private TimerStatus status; @Embedded - private Duration duration; + private TimerDuration timerDuration; @Builder - public Timer(Duration duration, TimerStatus status) { - this.duration = duration; + public Timer(TimerDuration timerDuration, TimerStatus status) { + this.timerDuration = timerDuration; this.status = status; } public void startTimer() { if (this.status == TimerStatus.STOPPED) { long startTime = System.currentTimeMillis(); - this.duration = new Duration(startTime, startTime); // 시작 시간과 종료 시간을 동일하게 설정 + this.timerDuration = new TimerDuration(startTime, startTime); // 시작 시간과 종료 시간을 동일하게 설정 this.status = TimerStatus.RUNNING; } else { // 이미 실행 중인 경우 처리 (예외 던지기 or 로깅) @@ -48,16 +48,16 @@ public void startTimer() { public void stopTimer() { if (this.status == TimerStatus.RUNNING) { long endTime = System.currentTimeMillis(); - this.duration = new Duration(this.duration.getStartTime(), endTime); // 종료 시간 업데이트 + this.timerDuration = new TimerDuration(this.timerDuration.getStartTime(), endTime); // 종료 시간 업데이트 this.status = TimerStatus.STOPPED; } else { // 이미 멈춘 경우 처리 (예외 던지기 or 로깅) } } - public void changeDuration(Duration newDuration) { + public void changeDuration(TimerDuration newTimerDuration) { if (this.status == TimerStatus.STOPPED) { // 타이머가 멈췄을 경우에만 지속시간 변경 가능 - this.duration = newDuration; + this.timerDuration = newTimerDuration; } else { // 이미 실행 중인 경우 변경 불가능 처리 (예외 던지기 or 로깅) } diff --git a/src/main/java/org/dnd/timeet/timer/domain/Duration.java b/src/main/java/org/dnd/timeet/timer/domain/TimerDuration.java similarity index 78% rename from src/main/java/org/dnd/timeet/timer/domain/Duration.java rename to src/main/java/org/dnd/timeet/timer/domain/TimerDuration.java index 08168c4..109d598 100644 --- a/src/main/java/org/dnd/timeet/timer/domain/Duration.java +++ b/src/main/java/org/dnd/timeet/timer/domain/TimerDuration.java @@ -10,12 +10,12 @@ @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Duration { // VO(값 객체) +public class TimerDuration { // VO(값 객체) private long startTime; private long endTime; - public Duration(long startTime, long endTime) { + public TimerDuration(long startTime, long endTime) { if (endTime < startTime) { throw new IllegalArgumentException("종료 시간은 시작 시간보다 뒤여야 합니다."); } @@ -32,11 +32,11 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof Duration)) { + if (!(o instanceof TimerDuration)) { return false; } - Duration duration = (Duration) o; - return startTime == duration.startTime && endTime == duration.endTime; + TimerDuration timerDuration = (TimerDuration) o; + return startTime == timerDuration.startTime && endTime == timerDuration.endTime; } @Override diff --git a/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java b/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java index 7a94358..59e20b0 100644 --- a/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java +++ b/src/main/java/org/dnd/timeet/timer/dto/TimerCreateRequest.java @@ -5,7 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.dnd.timeet.timer.domain.Duration; +import org.dnd.timeet.timer.domain.TimerDuration; import org.dnd.timeet.timer.domain.Timer; import org.dnd.timeet.timer.domain.TimerStatus; import org.springframework.format.annotation.DateTimeFormat; @@ -36,7 +36,7 @@ public class TimerCreateRequest { public Timer toEntity() { return Timer.builder() - .duration(new Duration(startTime.toInstant(ZoneOffset.UTC).toEpochMilli(), + .timerDuration(new TimerDuration(startTime.toInstant(ZoneOffset.UTC).toEpochMilli(), endTime.toInstant(ZoneOffset.UTC).toEpochMilli())) .status(this.status) .build(); diff --git a/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java b/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java index 61ef7f2..36379d7 100644 --- a/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java +++ b/src/main/java/org/dnd/timeet/timer/dto/TimerInfoResponse.java @@ -38,8 +38,8 @@ public static TimerInfoResponse from(Timer timer) { // 매개변수로부터 객 return TimerInfoResponse.builder() .id(timer.getId()) .status(timer.getStatus()) - .startTime(timer.getDuration().getStartTime()) - .endTime(timer.getDuration().getEndTime()) + .startTime(timer.getTimerDuration().getStartTime()) + .endTime(timer.getTimerDuration().getEndTime()) .build(); } } \ No newline at end of file From 73734fd12dbfd1672531bf86757e87e6a6fd481a Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 7 Feb 2024 23:13:11 +0900 Subject: [PATCH 19/81] [#13] refactor: Refactor codebase according to ERD updates --- .../java/org/dnd/timeet/config/SecurityConfig.java | 1 - .../java/org/dnd/timeet/meeting/domain/Meeting.java | 10 +++++++--- .../dnd/timeet/meeting/dto/MeetingCreateRequest.java | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dnd/timeet/config/SecurityConfig.java b/src/main/java/org/dnd/timeet/config/SecurityConfig.java index 2321d39..5fddfe3 100644 --- a/src/main/java/org/dnd/timeet/config/SecurityConfig.java +++ b/src/main/java/org/dnd/timeet/config/SecurityConfig.java @@ -51,7 +51,6 @@ public class SecurityConfig { "/api/v1/users/**", //h2-console "/h2-console/**", - "/api/meetings/**", }; diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index 39317bd..c494332 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -43,7 +43,7 @@ public class Meeting extends AuditableEntity { // 실제 소요 시간, 초기값 null private LocalTime totalActualDuration; - @Column(nullable = false, length = 255) + @Column(nullable = true, length = 255) private String location; @Column(nullable = true, length = 1000) @@ -53,14 +53,18 @@ public class Meeting extends AuditableEntity { @Column(nullable = false, length = 50) private MeetingStatus status = MeetingStatus.SCHEDULED; + @Column(nullable = false) + private Integer imgNum; + @Builder public Meeting(String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, String location, - String description) { + String description, Integer imgNum) { this.title = title; this.startTime = startTime; this.totalEstimatedDuration = totalEstimatedDuration; this.location = location; - this.description = description; // 입력한 값이 없을 경우 null + this.description = description; + this.imgNum = imgNum; } diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java index ea88276..a391b5d 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java @@ -22,7 +22,6 @@ public class MeetingCreateRequest { @Schema(description = "회의 제목", example = "2차 업무 회의") private String title; - @NotNull(message = "회의 장소는 반드시 입력되어야 합니다") @Schema(description = "회의 장소", example = "스타벅스 강남역점") private String location; From f9a56fca52cf25b97dcd5ae314d6ac82afdacc0b Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 8 Feb 2024 00:14:12 +0900 Subject: [PATCH 20/81] [#20] refactor: Specify ResponseEntity type for meeting API --- .../dnd/timeet/meeting/controller/MeetingController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index 0a31805..e90cd67 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -5,13 +5,12 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.utils.ApiUtils; +import org.dnd.timeet.common.utils.ApiUtils.ApiResult; import org.dnd.timeet.meeting.application.MeetingService; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; -import org.dnd.timeet.timer.domain.Timer; -import org.dnd.timeet.timer.dto.TimerInfoResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -30,7 +29,8 @@ public class MeetingController { @PostMapping @Operation(summary = "회의 생성", description = "회의를 생성한다.") - public ResponseEntity createMeeting(@RequestBody @Valid MeetingCreateRequest meetingCreateRequest) { + public ResponseEntity> createMeeting( + @RequestBody @Valid MeetingCreateRequest meetingCreateRequest) { // TODO : 유저 인증 로직 추가 Meeting savedMeeting = meetingService.createMeeting(meetingCreateRequest); MeetingCreateResponse meetingCreateResponse = MeetingCreateResponse.from(savedMeeting); @@ -40,7 +40,7 @@ public ResponseEntity createMeeting(@RequestBody @Valid MeetingCreateRequest @GetMapping("/{id}") @Operation(summary = "단일 회의 조회", description = "지정된 id에 해당하는 회의를 조회한다.") - public ResponseEntity getTimerById(@PathVariable("id") Long meetingId) { + public ResponseEntity> getTimerById(@PathVariable("id") Long meetingId) { Meeting meeting = meetingService.findById(meetingId); MeetingInfoResponse meetingInfoResponse = MeetingInfoResponse.from(meeting); From e733ec503b4637fcd0abe7f6b7d656e7ca809774 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 8 Feb 2024 11:15:42 +0900 Subject: [PATCH 21/81] [#12] feat: Add kakao-oauth and refactor member --- build.gradle | 1 + .../common/security/CustomUserDetails.java | 48 +++++++--- .../security/CustomUserDetailsService.java | 33 ------- .../timeet/common/security/JWTProvider.java | 14 +-- .../security/JwtAuthenticationFilter.java | 20 ++--- .../dnd/timeet/common/utils/CookieUtil.java | 71 +++++++++++++++ .../org/dnd/timeet/config/SecurityConfig.java | 30 ++++++- .../member/application/MemberFindService.java | 25 ++++++ .../member/application/MemberService.java | 17 ++++ .../member/controller/MemberController.java | 18 ++++ .../org/dnd/timeet/member/domain/Member.java | 51 +++++++++++ .../member/domain/MemberRepository.java | 12 +++ .../dnd/timeet/member/domain/MemberRole.java | 5 ++ .../timeet/member/dto/MemberInfoResponse.java | 34 +++++++ .../org/dnd/timeet/oauth/OAuth2Provider.java | 10 +++ .../application/CustomOAuth2UserService.java | 79 +++++++++++++++++ .../exception/OAuthProcessingException.java | 8 ++ .../OAuth2AuthenticationFailureHandler.java | 38 ++++++++ .../OAuth2AuthenticationSuccessHandler.java | 88 +++++++++++++++++++ .../dnd/timeet/oauth/info/OAuth2UserInfo.java | 24 +++++ .../oauth/info/OAuth2UserInfoFactory.java | 27 ++++++ .../oauth/info/impl/KakaoOAuth2UserInfo.java | 33 +++++++ .../user/application/UserFindService.java | 26 ------ .../timeet/user/application/UserService.java | 80 ----------------- .../user/controller/UserController.java | 69 --------------- .../java/org/dnd/timeet/user/domain/User.java | 45 ---------- .../timeet/user/domain/UserRepository.java | 11 --- .../org/dnd/timeet/user/domain/UserRole.java | 5 -- .../timeet/user/dto/EmailCheckRequest.java | 17 ---- .../user/dto/UpdatePasswordRequest.java | 19 ---- .../dnd/timeet/user/dto/UserInfoResponse.java | 38 -------- .../dnd/timeet/user/dto/UserLoginRequest.java | 24 ----- .../timeet/user/dto/UserLoginResponse.java | 17 ---- .../timeet/user/dto/UserRegisterRequest.java | 41 --------- 34 files changed, 622 insertions(+), 456 deletions(-) delete mode 100644 src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java create mode 100644 src/main/java/org/dnd/timeet/common/utils/CookieUtil.java create mode 100644 src/main/java/org/dnd/timeet/member/application/MemberFindService.java create mode 100644 src/main/java/org/dnd/timeet/member/application/MemberService.java create mode 100644 src/main/java/org/dnd/timeet/member/controller/MemberController.java create mode 100644 src/main/java/org/dnd/timeet/member/domain/Member.java create mode 100644 src/main/java/org/dnd/timeet/member/domain/MemberRepository.java create mode 100644 src/main/java/org/dnd/timeet/member/domain/MemberRole.java create mode 100644 src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java create mode 100644 src/main/java/org/dnd/timeet/oauth/OAuth2Provider.java create mode 100644 src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java create mode 100644 src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java create mode 100644 src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationFailureHandler.java create mode 100644 src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationSuccessHandler.java create mode 100644 src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfo.java create mode 100644 src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfoFactory.java create mode 100644 src/main/java/org/dnd/timeet/oauth/info/impl/KakaoOAuth2UserInfo.java delete mode 100644 src/main/java/org/dnd/timeet/user/application/UserFindService.java delete mode 100644 src/main/java/org/dnd/timeet/user/application/UserService.java delete mode 100644 src/main/java/org/dnd/timeet/user/controller/UserController.java delete mode 100644 src/main/java/org/dnd/timeet/user/domain/User.java delete mode 100644 src/main/java/org/dnd/timeet/user/domain/UserRepository.java delete mode 100644 src/main/java/org/dnd/timeet/user/domain/UserRole.java delete mode 100644 src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java delete mode 100644 src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java delete mode 100644 src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java delete mode 100644 src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java delete mode 100644 src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java delete mode 100644 src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java diff --git a/build.gradle b/build.gradle index 54abd53..33ba110 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' implementation("org.springframework.boot:spring-boot-devtools") + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java b/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java index 392513a..97e5faa 100644 --- a/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java +++ b/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java @@ -1,25 +1,35 @@ package org.dnd.timeet.common.security; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.dnd.timeet.user.domain.User; +import org.dnd.timeet.member.domain.Member; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; -import java.util.Collection; -import java.util.Collections; - -@RequiredArgsConstructor @Getter -public class CustomUserDetails implements UserDetails { +public class CustomUserDetails implements UserDetails, OAuth2User { - private final User user; + private final Member member; + private transient Map atrributes; + + public CustomUserDetails(Member member) { + this.member = member; + } + + public CustomUserDetails(Member member, Map attributes) { + this.member = member; + this.atrributes = attributes; + } + //// UserDetail Override @Override public Collection getAuthorities() { - String roleName = user.getRole().name(); + String roleName = member.getRole().name(); if (!roleName.startsWith("ROLE_")) { roleName = "ROLE_" + roleName; } @@ -28,14 +38,19 @@ public Collection getAuthorities() { @Override public String getPassword() { - return user.getPassword(); + return null; } @Override public String getUsername() { - return user.getEmail(); + return String.valueOf(member.getId()); } + public Long getId() { + return member.getId(); + } + + @Override public boolean isAccountNonExpired() { return true; @@ -55,4 +70,15 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return true; } + + // OAuth2User Override + @Override + public String getName() { + return String.valueOf(member.getId()); + } + + @Override + public Map getAttributes() { + return atrributes; + } } diff --git a/src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java b/src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java deleted file mode 100644 index fb5a574..0000000 --- a/src/main/java/org/dnd/timeet/common/security/CustomUserDetailsService.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.dnd.timeet.common.security; - - -import lombok.RequiredArgsConstructor; -import org.dnd.timeet.user.domain.User; -import org.dnd.timeet.user.domain.UserRepository; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class CustomUserDetailsService implements UserDetailsService { - - private final UserRepository userRepository; - - /** - * 가입된 유저에 대하여 세션을 생성하고 반환한다. - * - * @param email 검색할 사용자의 이메일 주소 - * @return 가입된 유저에 해당하는 세션 정보 - * @throws UsernameNotFoundException 주어진 이메일에 해당하는 유저를 찾을 수 없을 때 발생 - */ - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException(String.format("User with email: %s not found.", email))); - return new CustomUserDetails(user); - } - -} diff --git a/src/main/java/org/dnd/timeet/common/security/JWTProvider.java b/src/main/java/org/dnd/timeet/common/security/JWTProvider.java index a59e3bd..0944ac2 100644 --- a/src/main/java/org/dnd/timeet/common/security/JWTProvider.java +++ b/src/main/java/org/dnd/timeet/common/security/JWTProvider.java @@ -6,10 +6,9 @@ import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; -import org.dnd.timeet.user.domain.User; -import org.springframework.stereotype.Component; - import java.util.Date; +import org.dnd.timeet.member.domain.Member; +import org.springframework.stereotype.Component; @Component public class JWTProvider { @@ -17,14 +16,15 @@ public class JWTProvider { public static final Long EXP = 1000L * 60 * 60 * 48; // 48시간 public static final String TOKEN_PREFIX = "Bearer "; public static final String HEADER = "Authorization"; + public static final String SECRET = "MySecretKey"; - public static String create(User user) { + public static String create(Member member) { String jwt = JWT.create() - .withSubject(user.getEmail()) + .withSubject(String.valueOf(member.getId())) .withExpiresAt(new Date(System.currentTimeMillis() + EXP)) - .withClaim("id", user.getId()) - .withClaim("role", user.getRole().name()) + .withClaim("id", member.getId()) + .withClaim("role", member.getRole().name()) .sign(Algorithm.HMAC512(SECRET)); return TOKEN_PREFIX + jwt; diff --git a/src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java b/src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java index 3e34f7b..dba821f 100644 --- a/src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/dnd/timeet/common/security/JwtAuthenticationFilter.java @@ -8,15 +8,17 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; import lombok.extern.slf4j.Slf4j; import org.dnd.timeet.common.exception.ApiException; import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.InternalServerError; import org.dnd.timeet.common.exception.UnAuthorizedError; -import org.dnd.timeet.config.SecurityConfig; -import org.dnd.timeet.user.domain.User; import org.dnd.timeet.common.utils.ApiUtils; -import org.dnd.timeet.user.application.UserFindService; +import org.dnd.timeet.config.SecurityConfig; +import org.dnd.timeet.member.application.MemberFindService; +import org.dnd.timeet.member.domain.Member; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -24,15 +26,12 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import java.io.IOException; -import java.util.Collections; - @Slf4j public class JwtAuthenticationFilter extends BasicAuthenticationFilter { - private final UserFindService userUtilityService; + private final MemberFindService userUtilityService; - public JwtAuthenticationFilter(AuthenticationManager authenticationManager, UserFindService userUtilityService) { + public JwtAuthenticationFilter(AuthenticationManager authenticationManager, MemberFindService userUtilityService) { super(authenticationManager); this.userUtilityService = userUtilityService; } @@ -40,6 +39,7 @@ public JwtAuthenticationFilter(AuthenticationManager authenticationManager, User @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + String jwt = request.getHeader(JWTProvider.HEADER); try { @@ -47,9 +47,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse DecodedJWT decodedJWT = JWTProvider.verify(jwt); Long id = decodedJWT.getClaim("id").asLong(); - User user = userUtilityService.getUserById(id); + Member member = userUtilityService.getUserById(id); - CustomUserDetails myUserDetails = new CustomUserDetails(user); + CustomUserDetails myUserDetails = new CustomUserDetails(member); Authentication authentication = new UsernamePasswordAuthenticationToken( myUserDetails, diff --git a/src/main/java/org/dnd/timeet/common/utils/CookieUtil.java b/src/main/java/org/dnd/timeet/common/utils/CookieUtil.java new file mode 100644 index 0000000..ea739dc --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/utils/CookieUtil.java @@ -0,0 +1,71 @@ +package org.dnd.timeet.common.utils; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.util.Base64; +import java.util.Optional; +import org.springframework.util.SerializationUtils; + +public class CookieUtil { + + private CookieUtil() { + throw new IllegalStateException("Utility class"); + } + + public static Optional getCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return Optional.of(cookie); + } + } + } + return Optional.empty(); + } + + public static void addCookie(HttpServletResponse response, String name, String value, + int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + + response.addCookie(cookie); + } + + public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, + String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + cookie.setValue(""); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + } + } + + public static String serialize(Object object) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(object)); + } + + public static T deserialize(Cookie cookie, Class cls) { + try (ByteArrayInputStream bis = new ByteArrayInputStream( + Base64.getUrlDecoder().decode(cookie.getValue())); + ObjectInput in = new ObjectInputStream(bis)) { + return cls.cast(in.readObject()); + } catch (IOException | ClassNotFoundException e) { + throw new IllegalArgumentException("Failed to deserialize object", e); + } + } +} diff --git a/src/main/java/org/dnd/timeet/config/SecurityConfig.java b/src/main/java/org/dnd/timeet/config/SecurityConfig.java index e63f374..7126e97 100644 --- a/src/main/java/org/dnd/timeet/config/SecurityConfig.java +++ b/src/main/java/org/dnd/timeet/config/SecurityConfig.java @@ -2,11 +2,16 @@ import java.util.Collections; +import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.ForbiddenError; +import org.dnd.timeet.common.security.CookieAuthorizationRequestRepository; import org.dnd.timeet.common.security.CustomAuthenticationEntryPoint; import org.dnd.timeet.common.security.JwtAuthenticationFilter; -import org.dnd.timeet.user.application.UserFindService; import org.dnd.timeet.common.utils.FilterResponseUtils; +import org.dnd.timeet.member.application.MemberFindService; +import org.dnd.timeet.oauth.application.CustomOAuth2UserService; +import org.dnd.timeet.oauth.handler.OAuth2AuthenticationFailureHandler; +import org.dnd.timeet.oauth.handler.OAuth2AuthenticationSuccessHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -31,6 +36,7 @@ @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { @Value("${frontend.localurl}") @@ -40,7 +46,15 @@ public class SecurityConfig { private String prodfronturl; @Autowired - private UserFindService userUtilityService; + private MemberFindService userUtilityService; + + private final CustomOAuth2UserService customOAuth2UserService; + + private final CookieAuthorizationRequestRepository cookieAuthorizationRequestRepository; + + private final OAuth2AuthenticationSuccessHandler OAuth2AuthenticationSuccessHandler; + + private final OAuth2AuthenticationFailureHandler OAuth2AuthenticationFailureHandler; public static final String[] PUBLIC_URLS = { // swaggger url @@ -50,7 +64,8 @@ public class SecurityConfig { // open url "/api/v1/users/**", //h2-console - "/h2-console/**" + "/h2-console/**", + "oauth2/**", }; @@ -118,6 +133,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication .anyRequest().authenticated() ); + http.oauth2Login(oauth2 -> oauth2 + .authorizationEndpoint(authorization -> authorization + .authorizationRequestRepository(cookieAuthorizationRequestRepository)) + .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig + .userService(customOAuth2UserService) + ).successHandler(OAuth2AuthenticationSuccessHandler) + .failureHandler(OAuth2AuthenticationFailureHandler) + ); + return http.build(); } diff --git a/src/main/java/org/dnd/timeet/member/application/MemberFindService.java b/src/main/java/org/dnd/timeet/member/application/MemberFindService.java new file mode 100644 index 0000000..c97e6cb --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/application/MemberFindService.java @@ -0,0 +1,25 @@ +package org.dnd.timeet.member.application; + +import java.util.Collections; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class MemberFindService { + + private final MemberRepository memberRepository; + + @Autowired + public MemberFindService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public Member getUserById(Long id) throws Exception { + return memberRepository.findById(id) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("User", "User not found"))); + } +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/application/MemberService.java b/src/main/java/org/dnd/timeet/member/application/MemberService.java new file mode 100644 index 0000000..0fd9236 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/application/MemberService.java @@ -0,0 +1,17 @@ +package org.dnd.timeet.member.application; + + +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.member.domain.MemberRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class MemberService { + + private final PasswordEncoder passwordEncoder; + private final MemberRepository memberRepository; +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/controller/MemberController.java b/src/main/java/org/dnd/timeet/member/controller/MemberController.java new file mode 100644 index 0000000..c63ccd8 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/controller/MemberController.java @@ -0,0 +1,18 @@ +package org.dnd.timeet.member.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.member.application.MemberService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "User 컨트롤러", description = "User API입니다.") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class MemberController { + + private final MemberService memberService; + +} + diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java new file mode 100644 index 0000000..9d684c8 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -0,0 +1,51 @@ +package org.dnd.timeet.member.domain; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.dnd.timeet.common.domain.BaseEntity; +import org.dnd.timeet.oauth.OAuth2Provider; +import org.hibernate.annotations.Where; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +@Table(name = "member") +@Where(clause = "is_deleted=false") +@AttributeOverride(name = "id", column = @Column(name = "member_id")) +public class Member extends BaseEntity { + + @Enumerated(EnumType.STRING) + @Column(length = 50, nullable = false) + private MemberRole role; + + @Column(length = 100, nullable = false) + private String name; + + @Column(length = 255, nullable = false) + private String imageUrl; + + @Column(length = 100, nullable = false) + private Long oauthId; + + @Enumerated(EnumType.STRING) + @Column(length = 50, nullable = false) + private OAuth2Provider provider; + + // MEMO : 필수값들이므로 final 붙임 + @Builder + public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth2Provider provider) { + this.role = role; + this.name = name; + this.imageUrl = imageUrl; + this.oauthId = oauthId; + this.provider = provider; + } +} diff --git a/src/main/java/org/dnd/timeet/member/domain/MemberRepository.java b/src/main/java/org/dnd/timeet/member/domain/MemberRepository.java new file mode 100644 index 0000000..f260015 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/domain/MemberRepository.java @@ -0,0 +1,12 @@ +package org.dnd.timeet.member.domain; + + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + + Optional findById(Long id); + + Optional findByOauthId(Long oauthId); +} diff --git a/src/main/java/org/dnd/timeet/member/domain/MemberRole.java b/src/main/java/org/dnd/timeet/member/domain/MemberRole.java new file mode 100644 index 0000000..41dc096 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/domain/MemberRole.java @@ -0,0 +1,5 @@ +package org.dnd.timeet.member.domain; + +public enum MemberRole { + ROLE_USER, ROLE_OWNER, ROLE_ADMIN +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java b/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java new file mode 100644 index 0000000..ef336c4 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java @@ -0,0 +1,34 @@ +package org.dnd.timeet.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRole; + +@Getter +@Setter +public class MemberInfoResponse { + + @Schema(description = "사용자 id", nullable = false, example = "12") + private long id; + @Schema(description = "사용자 이름", nullable = false, example = "green12") + private String username; + @Schema(description = "사용자 역할", nullable = false, example = "ROLE_USER") + private MemberRole role; + + + public MemberInfoResponse(long id, String username, MemberRole role) { + this.id = id; + this.username = username; + this.role = role; + } + + public static MemberInfoResponse from(Member member) { + return new MemberInfoResponse( + member.getId(), + member.getName(), + member.getRole() + ); + } +} diff --git a/src/main/java/org/dnd/timeet/oauth/OAuth2Provider.java b/src/main/java/org/dnd/timeet/oauth/OAuth2Provider.java new file mode 100644 index 0000000..06ec892 --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/OAuth2Provider.java @@ -0,0 +1,10 @@ +package org.dnd.timeet.oauth; + +import lombok.Getter; + +@Getter +public enum OAuth2Provider { + KAKAO, + GOOGLE, + NAVER; +} diff --git a/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java b/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java new file mode 100644 index 0000000..485e444 --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java @@ -0,0 +1,79 @@ +package org.dnd.timeet.oauth.application; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.security.CustomUserDetails; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRepository; +import org.dnd.timeet.member.domain.MemberRole; +import org.dnd.timeet.oauth.OAuth2Provider; +import org.dnd.timeet.oauth.exception.OAuthProcessingException; +import org.dnd.timeet.oauth.info.OAuth2UserInfo; +import org.dnd.timeet.oauth.info.OAuth2UserInfoFactory; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oauth2User = super.loadUser(userRequest); + + try { + return processOAuth2User(userRequest, oauth2User); + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + throw new InternalAuthenticationServiceException(e.getMessage(), e.getCause()); + } + } + + private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) { + + OAuth2Provider oauth2Provider = OAuth2Provider.valueOf( + userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2Userinfo(oauth2Provider, + oauth2User.getAttributes()); + + if (userInfo.getId() == null) { + throw new OAuthProcessingException("ID not found from OAuth2 provider"); + } + + Optional userOptional = memberRepository.findByOauthId(userInfo.getId()); + + Member member; + if (userOptional.isPresent()) { + member = userOptional.get(); + if (oauth2Provider != member.getProvider()) { + throw new OAuthProcessingException("Wrong Match Auth Provider"); + } + + } else { + member = createUser(userInfo, oauth2Provider); + } + + return new CustomUserDetails(member, oauth2User.getAttributes()); + } + + private Member createUser(OAuth2UserInfo userInfo, OAuth2Provider oauth2Provider) { + return memberRepository.save( + Member.builder() + .name(userInfo.getName()) + .imageUrl(userInfo.getImageUrl()) + .role(MemberRole.ROLE_USER) + .provider(oauth2Provider) + .oauthId(userInfo.getId()) + .build() + ); + } + +} diff --git a/src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java b/src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java new file mode 100644 index 0000000..eb00077 --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java @@ -0,0 +1,8 @@ +package org.dnd.timeet.oauth.exception; + +public class OAuthProcessingException extends RuntimeException { + + public OAuthProcessingException(String message) { + super(message); + } +} diff --git a/src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationFailureHandler.java b/src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationFailureHandler.java new file mode 100644 index 0000000..2b1f6db --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationFailureHandler.java @@ -0,0 +1,38 @@ +package org.dnd.timeet.oauth.handler; + +import static org.dnd.timeet.common.security.CookieAuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.security.CookieAuthorizationRequestRepository; +import org.dnd.timeet.common.utils.CookieUtil; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +@RequiredArgsConstructor +public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + private final CookieAuthorizationRequestRepository authorizationRequestRepository; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) + throws IOException { + String targetUrl = CookieUtil.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue) + .orElse("/"); + + targetUrl = UriComponentsBuilder.fromUriString(targetUrl) + .queryParam("error", exception.getLocalizedMessage()) + .build().toUriString(); + + authorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} diff --git a/src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationSuccessHandler.java b/src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000..9574841 --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/handler/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,88 @@ +package org.dnd.timeet.oauth.handler; + +import static org.dnd.timeet.common.security.CookieAuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.common.exception.BadRequestError.ErrorCode; +import org.dnd.timeet.common.security.CookieAuthorizationRequestRepository; +import org.dnd.timeet.common.security.CustomUserDetails; +import org.dnd.timeet.common.security.JWTProvider; +import org.dnd.timeet.common.utils.CookieUtil; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +@RequiredArgsConstructor +@Slf4j +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + @Value("${app.oauth2.authorized-redirect-uri}") + private String redirectUri; + + private final CookieAuthorizationRequestRepository authorizationRequestRepository; + private final MemberRepository memberRepository; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + String targetUrl = determineTargetUrl(request, response, authentication); + + if (response.isCommitted()) { + return; + } + clearAuthenticationAttributes(request, response); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } + + @Override + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + Optional redirect = CookieUtil.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue); + + if (redirect.isPresent() && !isAuthorizedRedirectUri(redirect.get())) { + throw new BadRequestError(ErrorCode.VALIDATION_FAILED, + Collections.singletonMap("redirect", "Unauthorized redirect uri")); + } + String targetUrl = redirect.orElse(getDefaultTargetUrl()); + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + Optional user = memberRepository.findById(userDetails.getId()); + + // jwt + + String accessToken = JWTProvider.create(user.get()); + + return UriComponentsBuilder.fromUriString(targetUrl) + .queryParam("code", accessToken) + .build().toUriString(); + } + + protected void clearAuthenticationAttributes(HttpServletRequest request, + HttpServletResponse response) { + super.clearAuthenticationAttributes(request); + authorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + } + + private boolean isAuthorizedRedirectUri(String uri) { + URI clientRedirectUri = URI.create(uri); + URI authorizedUri = URI.create(redirectUri); + + return authorizedUri.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) + && authorizedUri.getPort() == clientRedirectUri.getPort(); + } +} diff --git a/src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfo.java b/src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfo.java new file mode 100644 index 0000000..d1c030f --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfo.java @@ -0,0 +1,24 @@ +package org.dnd.timeet.oauth.info; + +import java.util.Map; + +public abstract class OAuth2UserInfo { + + protected final Map attributes; + + protected OAuth2UserInfo(Map attributes) { + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } + + public abstract Long getId(); + + public abstract String getName(); + + + public abstract String getImageUrl(); + +} diff --git a/src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfoFactory.java b/src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfoFactory.java new file mode 100644 index 0000000..3992e59 --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/info/OAuth2UserInfoFactory.java @@ -0,0 +1,27 @@ +package org.dnd.timeet.oauth.info; + +import java.util.Map; +import org.dnd.timeet.oauth.OAuth2Provider; +import org.dnd.timeet.oauth.info.impl.KakaoOAuth2UserInfo; + +public class OAuth2UserInfoFactory { + + private OAuth2UserInfoFactory() { + throw new IllegalStateException("Utility class"); + } + + public static OAuth2UserInfo getOAuth2Userinfo(OAuth2Provider oauth2Provider, + Map attributes) { + + if (oauth2Provider == OAuth2Provider.GOOGLE) { + return null; + } + if (oauth2Provider == OAuth2Provider.NAVER) { + return null; + } + if (oauth2Provider == OAuth2Provider.KAKAO) { + return new KakaoOAuth2UserInfo(attributes); + } + throw new IllegalArgumentException("Invalid AuthProvider Type."); + } +} diff --git a/src/main/java/org/dnd/timeet/oauth/info/impl/KakaoOAuth2UserInfo.java b/src/main/java/org/dnd/timeet/oauth/info/impl/KakaoOAuth2UserInfo.java new file mode 100644 index 0000000..8ffb30d --- /dev/null +++ b/src/main/java/org/dnd/timeet/oauth/info/impl/KakaoOAuth2UserInfo.java @@ -0,0 +1,33 @@ +package org.dnd.timeet.oauth.info.impl; + +import java.util.Map; +import org.dnd.timeet.oauth.info.OAuth2UserInfo; + +public class KakaoOAuth2UserInfo extends OAuth2UserInfo { + + private final Map kakaoAccount; + private final Map profile; + + public KakaoOAuth2UserInfo(Map attributes) { + super(attributes); + + this.kakaoAccount = (Map) attributes.get("kakao_account"); + this.profile = (Map) kakaoAccount.get("profile"); + } + + @Override + public Long getId() { + return (Long) attributes.get("id"); + } + + @Override + public String getName() { + return (String) this.profile.get("nickname"); + } + + @Override + public String getImageUrl() { + return (String) this.profile.get("profile_image_url"); + } + +} diff --git a/src/main/java/org/dnd/timeet/user/application/UserFindService.java b/src/main/java/org/dnd/timeet/user/application/UserFindService.java deleted file mode 100644 index 4269e7a..0000000 --- a/src/main/java/org/dnd/timeet/user/application/UserFindService.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.dnd.timeet.user.application; - -import org.dnd.timeet.common.exception.NotFoundError; -import org.dnd.timeet.user.domain.User; -import org.dnd.timeet.user.domain.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.Collections; - -@Service -public class UserFindService { - - private final UserRepository userRepository; - - @Autowired - public UserFindService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public User getUserById(Long id) throws Exception { - return userRepository.findById(id) - .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("User", "User not found"))); - } -} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/user/application/UserService.java b/src/main/java/org/dnd/timeet/user/application/UserService.java deleted file mode 100644 index a8e352a..0000000 --- a/src/main/java/org/dnd/timeet/user/application/UserService.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.dnd.timeet.user.application; - - -import lombok.RequiredArgsConstructor; -import org.dnd.timeet.common.exception.BadRequestError; -import org.dnd.timeet.common.exception.InternalServerError; -import org.dnd.timeet.common.exception.NotFoundError; -import org.dnd.timeet.common.exception.UnAuthorizedError; -import org.dnd.timeet.common.security.JWTProvider; -import org.dnd.timeet.user.domain.User; -import org.dnd.timeet.user.domain.UserRepository; -import org.dnd.timeet.user.dto.UserLoginRequest; -import org.dnd.timeet.user.dto.UserLoginResponse; -import org.dnd.timeet.user.dto.UserRegisterRequest; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collections; -import java.util.Optional; - -@Transactional(readOnly = true) -@RequiredArgsConstructor -@Service -public class UserService { - - private final PasswordEncoder passwordEncoder; - private final UserRepository userRepository; - - @Transactional - public void register(UserRegisterRequest userRegisterRequest) { - checkSameEmail(userRegisterRequest.getEmail()); - - String encodedPassword = passwordEncoder.encode(userRegisterRequest.getPassword()); - try { - userRepository.save(userRegisterRequest.toEntity(encodedPassword)); - } catch (Exception e) { - throw new InternalServerError( - InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, - Collections.singletonMap("error", "Unknown server error occurred.")); - } - } - - public void checkSameEmail(String email) { - Optional memberOptional = userRepository.findByEmail(email); - if (memberOptional.isPresent()) { - throw new BadRequestError(BadRequestError.ErrorCode.DUPLICATE_RESOURCE, - Collections.singletonMap("Email", "Duplicate email exist : " + email)); - } - } - - public UserLoginResponse login(UserLoginRequest requestDTO) { - User user = userRepository.findByEmail(requestDTO.getEmail()).orElseThrow( - () -> new NotFoundError( - NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("Email", "email not found : " + requestDTO.getEmail()) - )); - if (!passwordEncoder.matches(requestDTO.getPassword(), user.getPassword())) { - throw new UnAuthorizedError( - UnAuthorizedError.ErrorCode.AUTHENTICATION_FAILED, - Collections.singletonMap("Password", "Wrong password") - ); - } - - String jwt = JWTProvider.create(user); - String redirectUrl = "/user/home"; - - return new UserLoginResponse(jwt, redirectUrl); - } - -// public UserInfoResponse findUser(User user) { -// User findUser = userRepository.findById(user.getId()) -// .orElseThrow(() -> new NotFoundError( -// NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, -// Collections.singletonMap("UserId", "User is not found.") -// )); -// -// return UserInfoResponse.from(findUser); -// } -} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/user/controller/UserController.java b/src/main/java/org/dnd/timeet/user/controller/UserController.java deleted file mode 100644 index 846491b..0000000 --- a/src/main/java/org/dnd/timeet/user/controller/UserController.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.dnd.timeet.user.controller; - -import io.swagger.v3.oas.annotations.Operation; -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; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.dnd.timeet.common.exception.BadRequestError; -import org.dnd.timeet.common.exception.InternalServerError; -import org.dnd.timeet.common.security.JWTProvider; -import org.dnd.timeet.user.application.UserService; -import org.dnd.timeet.user.dto.EmailCheckRequest; -import org.dnd.timeet.user.dto.UserLoginRequest; -import org.dnd.timeet.user.dto.UserLoginResponse; -import org.dnd.timeet.user.dto.UserRegisterRequest; -import org.dnd.timeet.common.utils.ApiUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "User 컨트롤러", description = "User API입니다.") -@RequiredArgsConstructor -@RestController -@RequestMapping("/api/v1/users") -public class UserController { - - private final UserService userService; - - @PostMapping("/check") - @Operation(summary = "이메일 중복 검사", description = "입력받은 이메일 주소가 이미 사용 중인지 확인한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "이메일 사용 가능"), - @ApiResponse(responseCode = "400", description = "중복된 이메일 존재 또는 잘못된 요청", content = @Content(schema = @Schema(implementation = BadRequestError.class))), - @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = InternalServerError.class))) - }) - public ResponseEntity checkEmail(@RequestBody @Valid EmailCheckRequest emailCheckDTO) { - userService.checkSameEmail(emailCheckDTO.getEmail()); - - return ResponseEntity.ok(ApiUtils.success(null)); - } - - @PostMapping - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "유저 등록 성공"), - @ApiResponse(responseCode = "400", description = "중복된 이메일 존재 또는 잘못된 요청", content = @Content(schema = @Schema(implementation = BadRequestError.class))), - @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = InternalServerError.class))) - }) - @Operation(summary = "유저 등록", description = "새롭게 유저를 등록한다.") - public ResponseEntity registerUser(@RequestBody @Valid UserRegisterRequest requestDTO) { - userService.register(requestDTO); - - return ResponseEntity.ok().body(ApiUtils.success(null)); - } - - @PostMapping("/login") - @Operation(summary = "유저 로그인", description = "사용자의 이메일과 비밀번호를 받아 로그인을 처리한다.") - public ResponseEntity loginUser(@RequestBody @Valid UserLoginRequest requestDTO) { - UserLoginResponse response = userService.login(requestDTO); - - return ResponseEntity.ok().header(JWTProvider.HEADER, response.getJwtToken()).body(ApiUtils.success(null)); - } - -} - diff --git a/src/main/java/org/dnd/timeet/user/domain/User.java b/src/main/java/org/dnd/timeet/user/domain/User.java deleted file mode 100644 index 9c0e856..0000000 --- a/src/main/java/org/dnd/timeet/user/domain/User.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.dnd.timeet.user.domain; - -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.dnd.timeet.common.domain.BaseEntity; -import org.hibernate.annotations.Where; - -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -@Entity -@Table(name = "member") -@Where(clause = "is_deleted=false") -@AttributeOverride(name = "id", column = @Column(name = "user_id")) -public class User extends BaseEntity { - - @Enumerated(EnumType.STRING) - @Column(length = 30, nullable = false) - private UserRole role; - - @Column(length = 20, nullable = false) - private String name; - - @Column(length = 50, nullable = false, unique = true) - private String email; - - @Column(length = 255, nullable = false) // 암호화된 비밀번호가 저장됨 (255글자) - private String password; - - // MEMO : 필수값들이므로 final 붙임 - @Builder - public User(UserRole role, String name, String email, String password) { - this.role = role; - this.name = name; - this.email = email; - this.password = password; - } -} diff --git a/src/main/java/org/dnd/timeet/user/domain/UserRepository.java b/src/main/java/org/dnd/timeet/user/domain/UserRepository.java deleted file mode 100644 index 3e9a949..0000000 --- a/src/main/java/org/dnd/timeet/user/domain/UserRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.dnd.timeet.user.domain; - - -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - - Optional findByEmail(String email); -} diff --git a/src/main/java/org/dnd/timeet/user/domain/UserRole.java b/src/main/java/org/dnd/timeet/user/domain/UserRole.java deleted file mode 100644 index 610dc42..0000000 --- a/src/main/java/org/dnd/timeet/user/domain/UserRole.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.dnd.timeet.user.domain; - -public enum UserRole { - ROLE_USER, ROLE_OWNER, ROLE_ADMIN -} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java b/src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java deleted file mode 100644 index 7a412df..0000000 --- a/src/main/java/org/dnd/timeet/user/dto/EmailCheckRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.dnd.timeet.user.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class EmailCheckRequest { - - @NotBlank(message = "email is required.") - @Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "Please enter a valid email address") - @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") - private String email; -} diff --git a/src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java b/src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java deleted file mode 100644 index 5e1136d..0000000 --- a/src/main/java/org/dnd/timeet/user/dto/UpdatePasswordRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.dnd.timeet.user.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UpdatePasswordRequest { - - @NotBlank(message = "password is required.") - @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters") - @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=!~`<>,./?;:'\"\\[\\]{}\\\\()|_-])\\S*$", message = "It must contain letters, numbers, and special characters, and cannot contain spaces") - @Schema(description = "사용자 비밀번호", nullable = false, example = "green1234!") - private String password; -} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java b/src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java deleted file mode 100644 index 6ff1e2f..0000000 --- a/src/main/java/org/dnd/timeet/user/dto/UserInfoResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.dnd.timeet.user.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.Setter; -import org.dnd.timeet.user.domain.User; -import org.dnd.timeet.user.domain.UserRole; - -@Getter -@Setter -public class UserInfoResponse { - - @Schema(description = "사용자 id", nullable = false, example = "12") - private long id; - @Schema(description = "사용자 이름", nullable = false, example = "green12") - private String username; - @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") - private String email; - @Schema(description = "사용자 역할", nullable = false, example = "ROLE_USER") - private UserRole role; - - - public UserInfoResponse(long id, String username, String email, UserRole role) { - this.id = id; - this.username = username; - this.email = email; - this.role = role; - } - - public static UserInfoResponse from(User user) { - return new UserInfoResponse( - user.getId(), - user.getName(), - user.getEmail(), - user.getRole() - ); - } -} diff --git a/src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java b/src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java deleted file mode 100644 index 32d0b3c..0000000 --- a/src/main/java/org/dnd/timeet/user/dto/UserLoginRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.dnd.timeet.user.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UserLoginRequest { - - @NotBlank(message = "email is required.") - @Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "Please enter a valid email address.") - @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") - private String email; - - @NotBlank(message = "password is required.") - @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters.") - @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=!~`<>,./?;:'\"\\[\\]{}\\\\()|_-])\\S*$", message = "It must contain letters, numbers, and special characters, and cannot contain spaces.") - @Schema(description = "사용자 비밀번호", nullable = false, example = "green1234!") - private String password; -} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java b/src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java deleted file mode 100644 index 917c7b1..0000000 --- a/src/main/java/org/dnd/timeet/user/dto/UserLoginResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.dnd.timeet.user.dto; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UserLoginResponse { - - private String jwtToken; - private String redirectUrl; - - public UserLoginResponse(String jwtToken, String redirectUrl) { - this.jwtToken = jwtToken; - this.redirectUrl = redirectUrl; - } -} diff --git a/src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java b/src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java deleted file mode 100644 index d489b08..0000000 --- a/src/main/java/org/dnd/timeet/user/dto/UserRegisterRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.dnd.timeet.user.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.Getter; -import lombok.Setter; -import org.dnd.timeet.user.domain.User; -import org.dnd.timeet.user.domain.UserRole; - -@Getter -@Setter -public class UserRegisterRequest { - - @NotBlank(message = "email is required.") - @Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "Please enter a valid email address") - @Schema(description = "사용자 이메일", nullable = false, example = "green12@gmail.com") - private String email; - - @NotBlank(message = "password is required.") - @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters") - @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=!~`<>,./?;:'\"\\[\\]{}\\\\()|_-])\\S*$", message = "It must contain letters, numbers, and special characters, and cannot contain spaces") - @Schema(description = "사용자 비밀번호", nullable = false, example = "green1234!") - private String password; - - @NotBlank(message = "name is required.") - @Size(min = 2, max = 20, message = "name must be between 2 and 20 characters") - @Schema(description = "사용자 이름", nullable = false, example = "green12") - private String name; - - - public User toEntity(String encodedPassword) { - return User.builder() - .email(email) - .password(encodedPassword) - .name(name) - .role(UserRole.ROLE_USER) // 기본적으로 User로 생성 - .build(); - } -} From f1d62ea7a44efbcb07f3c535859eb6d31b57891c Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 8 Feb 2024 11:23:38 +0900 Subject: [PATCH 22/81] [#12] feat: Add CookieAuthorizationRequestRepository --- .../CookieAuthorizationRequestRepository.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/common/security/CookieAuthorizationRequestRepository.java diff --git a/src/main/java/org/dnd/timeet/common/security/CookieAuthorizationRequestRepository.java b/src/main/java/org/dnd/timeet/common/security/CookieAuthorizationRequestRepository.java new file mode 100644 index 0000000..d2438c1 --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/security/CookieAuthorizationRequestRepository.java @@ -0,0 +1,57 @@ +package org.dnd.timeet.common.security; + +import com.nimbusds.oauth2.sdk.util.StringUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.dnd.timeet.common.utils.CookieUtil; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Component; + +@Component +public class CookieAuthorizationRequestRepository implements AuthorizationRequestRepository { + + public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; + private static final int COOKIE_EXPIRE_SECONDS = 180; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + return CookieUtil.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(cookie -> CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class)) + .orElse(null); + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, + HttpServletRequest request, HttpServletResponse response) { + if (authorizationRequest == null) { + CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + return; + } + + CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, + CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS); + String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); + + if (StringUtils.isNotBlank(redirectUriAfterLogin)) { + CookieUtil.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, + COOKIE_EXPIRE_SECONDS); + } + + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, + HttpServletResponse response) { + return this.loadAuthorizationRequest(request); + } + + public void removeAuthorizationRequestCookies(HttpServletRequest request, + HttpServletResponse response) { + CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + } + +} From 55e12497bb403169a528892ce15ff3cd74a4a169 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 8 Feb 2024 11:37:44 +0900 Subject: [PATCH 23/81] [#16] chore: Fix MemberController description and mapping user to member --- .../org/dnd/timeet/member/controller/MemberController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dnd/timeet/member/controller/MemberController.java b/src/main/java/org/dnd/timeet/member/controller/MemberController.java index c63ccd8..6065921 100644 --- a/src/main/java/org/dnd/timeet/member/controller/MemberController.java +++ b/src/main/java/org/dnd/timeet/member/controller/MemberController.java @@ -6,10 +6,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "User 컨트롤러", description = "User API입니다.") +@Tag(name = "Member 컨트롤러", description = "Member API입니다.") @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/users") +@RequestMapping("/api/v1/members") public class MemberController { private final MemberService memberService; From 754bbef0d925b037e96241b97d9eaa8d5683e3d0 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 8 Feb 2024 22:43:55 +0900 Subject: [PATCH 24/81] [#16] chore: Add Value annotation for SECRET key --- .../java/org/dnd/timeet/common/security/JWTProvider.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dnd/timeet/common/security/JWTProvider.java b/src/main/java/org/dnd/timeet/common/security/JWTProvider.java index 0944ac2..d6762ed 100644 --- a/src/main/java/org/dnd/timeet/common/security/JWTProvider.java +++ b/src/main/java/org/dnd/timeet/common/security/JWTProvider.java @@ -8,6 +8,7 @@ import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; import org.dnd.timeet.member.domain.Member; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @@ -17,7 +18,13 @@ public class JWTProvider { public static final String TOKEN_PREFIX = "Bearer "; public static final String HEADER = "Authorization"; - public static final String SECRET = "MySecretKey"; + + public static String SECRET; + + @Value("${app.auth.token.secret-key}") + public void setSecret(String secret) { + SECRET = secret; + } public static String create(Member member) { String jwt = JWT.create() From 8b2ed0d10476057476f828b94aa33a9f33f21984 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 8 Feb 2024 22:44:50 +0900 Subject: [PATCH 25/81] [#16] chore: Add Exception for Unsupported method in CustomUserDetails --- .../java/org/dnd/timeet/common/security/CustomUserDetails.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java b/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java index 97e5faa..047b833 100644 --- a/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java +++ b/src/main/java/org/dnd/timeet/common/security/CustomUserDetails.java @@ -38,7 +38,7 @@ public Collection getAuthorities() { @Override public String getPassword() { - return null; + throw new UnsupportedOperationException("getPassword() is not supported."); } @Override From 77a1fc37550720a06a6ff224da64a040b6c6bc6c Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 8 Feb 2024 22:50:37 +0900 Subject: [PATCH 26/81] [#16] chore: Add Internal Custom Error for CustomOAuth2UserService --- .../oauth/application/CustomOAuth2UserService.java | 11 ++++++++--- .../oauth/exception/OAuthProcessingException.java | 8 -------- 2 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java diff --git a/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java b/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java index 485e444..ae6598d 100644 --- a/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java +++ b/src/main/java/org/dnd/timeet/oauth/application/CustomOAuth2UserService.java @@ -1,13 +1,14 @@ package org.dnd.timeet.oauth.application; +import java.util.Collections; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.InternalServerError; import org.dnd.timeet.common.security.CustomUserDetails; import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.member.domain.MemberRepository; import org.dnd.timeet.member.domain.MemberRole; import org.dnd.timeet.oauth.OAuth2Provider; -import org.dnd.timeet.oauth.exception.OAuthProcessingException; import org.dnd.timeet.oauth.info.OAuth2UserInfo; import org.dnd.timeet.oauth.info.OAuth2UserInfoFactory; import org.springframework.security.authentication.InternalAuthenticationServiceException; @@ -45,7 +46,9 @@ private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User o oauth2User.getAttributes()); if (userInfo.getId() == null) { - throw new OAuthProcessingException("ID not found from OAuth2 provider"); + throw new InternalServerError( + InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("Id", "Id not found from OAuth2 provider")); } Optional userOptional = memberRepository.findByOauthId(userInfo.getId()); @@ -54,7 +57,9 @@ private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User o if (userOptional.isPresent()) { member = userOptional.get(); if (oauth2Provider != member.getProvider()) { - throw new OAuthProcessingException("Wrong Match Auth Provider"); + throw new InternalServerError( + InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("Provider", "Wrong Match Auth Provider")); } } else { diff --git a/src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java b/src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java deleted file mode 100644 index eb00077..0000000 --- a/src/main/java/org/dnd/timeet/oauth/exception/OAuthProcessingException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.dnd.timeet.oauth.exception; - -public class OAuthProcessingException extends RuntimeException { - - public OAuthProcessingException(String message) { - super(message); - } -} From 81eed9a08a157674fc416f23d7a4d8e88af2a035 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 9 Feb 2024 13:33:37 +0900 Subject: [PATCH 27/81] [#22] feat: Add FCM service and config --- .gitignore | 1 + build.gradle | 1 + .../java/org/dnd/timeet/config/FCMConfig.java | 41 ++++++++++++++ .../application/FCMNotificationService.java | 55 +++++++++++++++++++ .../fcm/domain/FCMNotificationRequestDto.java | 22 ++++++++ .../org/dnd/timeet/member/domain/Member.java | 3 + 6 files changed, 123 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/config/FCMConfig.java create mode 100644 src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java create mode 100644 src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java diff --git a/.gitignore b/.gitignore index 74f33d8..f8fb2e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +timeet-firebase-adminsdk.json HELP.md .gradle build/ diff --git a/build.gradle b/build.gradle index 33ba110..5c4732a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' implementation("org.springframework.boot:spring-boot-devtools") implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'com.google.firebase:firebase-admin:9.2.0' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/org/dnd/timeet/config/FCMConfig.java b/src/main/java/org/dnd/timeet/config/FCMConfig.java new file mode 100644 index 0000000..ba1f36d --- /dev/null +++ b/src/main/java/org/dnd/timeet/config/FCMConfig.java @@ -0,0 +1,41 @@ +package org.dnd.timeet.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +@Configuration +public class FCMConfig { + + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("timeet-firebase-adminsdk.json"); + + InputStream refreshToken = resource.getInputStream(); + + FirebaseApp firebaseApp = null; + List firebaseAppList = FirebaseApp.getApps(); + + if (firebaseAppList != null && !firebaseAppList.isEmpty()) { + for (FirebaseApp app : firebaseAppList) { + if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) { + firebaseApp = app; + } + } + } else { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(refreshToken)).build(); + firebaseApp = FirebaseApp.initializeApp(options); + } + return FirebaseMessaging.getInstance(firebaseApp); + } + + +} diff --git a/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java new file mode 100644 index 0000000..556d75b --- /dev/null +++ b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java @@ -0,0 +1,55 @@ +package org.dnd.timeet.fcm.application; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import java.util.Collections; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.InternalServerError; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.fcm.domain.FCMNotificationRequestDto; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class FCMNotificationService { + + private final FirebaseMessaging firebaseMessaging; + private final MemberRepository memberRepository; + + public void sendNotificationByToken(FCMNotificationRequestDto requestDto) { + Optional member = memberRepository.findById(requestDto.getTargetMemberId()); + + if (member.isEmpty()) { + throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found")); + } + + if (member.get().getFcmToken() == null) { + throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("fcmToken", "fcmToken not exist")); + } + + Notification notification = Notification.builder() + .setTitle(requestDto.getTitle()) + .setBody(requestDto.getBody()) + .build(); + + Message message = Message.builder() + .setToken(member.get().getFcmToken()) + .setNotification(notification) + .build(); + + try { + firebaseMessaging.send(message); + } catch (Exception e) { + throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("fcmSend", "Fail to send fcm")); + } + + + } +} diff --git a/src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java b/src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java new file mode 100644 index 0000000..710f56c --- /dev/null +++ b/src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java @@ -0,0 +1,22 @@ +package org.dnd.timeet.fcm.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class FCMNotificationRequestDto { + + private Long targetMemberId; + private String title; + private String body; + + @Builder + public FCMNotificationRequestDto(Long targetMemberId, String title, String body) { + this.targetMemberId = targetMemberId; + this.title = title; + this.body = body; + } + +} diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 9d684c8..1783892 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -39,6 +39,9 @@ public class Member extends BaseEntity { @Column(length = 50, nullable = false) private OAuth2Provider provider; + @Column(length = 255) + private String fcmToken; + // MEMO : 필수값들이므로 final 붙임 @Builder public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth2Provider provider) { From 388ddc283cb3623e2e9ea8fe7f366d2d27bdc4e7 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 17:26:17 +0900 Subject: [PATCH 28/81] [#13] refactor: Refactor codebase according to ERD updates --- .../org/dnd/timeet/meeting/dto/MeetingCreateRequest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java index a391b5d..8470e6f 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java @@ -38,6 +38,10 @@ public class MeetingCreateRequest { @Schema(description = "예상 소요 시간", example = "02:00") private LocalTime estimatedTotalDuration; + @NotNull(message = "썸네일은 반드시 입력되어야 합니다") + @Schema(description = "썸네일 이미지 번호", example = "1") + private Integer imageNum; + public Meeting toEntity() { return Meeting.builder() .title(this.title) @@ -45,6 +49,7 @@ public Meeting toEntity() { .startTime(startTime) .description(this.description) .totalEstimatedDuration(this.estimatedTotalDuration) + .imgNum(imageNum) .build(); } } From 64c0d24384b3764a793582cc36994494519620a1 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 18:21:15 +0900 Subject: [PATCH 29/81] [#13] feat: Modify entities to align with ERD and create agenda entity --- .../agenda/application/AgendaService.java | 33 ++++++++ .../agenda/controller/AgendaController.java | 37 +++++++++ .../org/dnd/timeet/agenda/domain/Agenda.java | 78 +++++++++++++++++++ .../agenda/domain/AgendaRepository.java | 8 ++ .../timeet/agenda/domain/AgendaStatus.java | 5 ++ .../dnd/timeet/agenda/domain/AgendaType.java | 5 ++ .../agenda/dto/AgendaCreateRequest.java | 54 +++++++++++++ .../agenda/dto/AgendaCreateResponse.java | 37 +++++++++ .../timeet/agenda/dto/AgendaInfoResponse.java | 38 +++++++++ .../dnd/timeet/meeting/domain/Meeting.java | 28 ++++++- .../org/dnd/timeet/member/domain/Member.java | 4 +- .../org/dnd/timeet/timer/domain/Timer.java | 1 + 12 files changed, 322 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/agenda/application/AgendaService.java create mode 100644 src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java create mode 100644 src/main/java/org/dnd/timeet/agenda/domain/Agenda.java create mode 100644 src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java create mode 100644 src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java create mode 100644 src/main/java/org/dnd/timeet/agenda/domain/AgendaType.java create mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java create mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java create mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java new file mode 100644 index 0000000..d27ac20 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -0,0 +1,33 @@ +package org.dnd.timeet.agenda.application; + +import java.util.Collections; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.agenda.domain.AgendaRepository; +import org.dnd.timeet.agenda.dto.AgendaCreateRequest; +import org.dnd.timeet.common.exception.NotFoundError; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor // final 의존성 주입 +@Transactional // DB 변경 작업에 사용 +public class AgendaService { + + private final AgendaRepository meetingRepository; + + public Agenda createMeeting(AgendaCreateRequest createDto) { + Agenda meeting = createDto.toEntity(); + // 복잡한 비즈니스 로직은 도메인 메서드를 이용하여 Service에서 처리 + return meetingRepository.save(meeting); + } + + @Transactional(readOnly = true) + public Agenda findById(Long id) { + return meetingRepository.findById(id) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + } + + +} diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java new file mode 100644 index 0000000..93b2221 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -0,0 +1,37 @@ +package org.dnd.timeet.agenda.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.agenda.application.AgendaService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "안건 컨트롤러", description = "Agenda API입니다.") +@RestController +@RequestMapping("/api/agendas") +@RequiredArgsConstructor +public class AgendaController { + + private final AgendaService agendaService; + +// @PostMapping +// @Operation(summary = "회의 생성", description = "회의를 생성한다.") +// public ResponseEntity> createMeeting( +// @RequestBody @Valid MeetingCreateRequest meetingCreateRequest) { +// // TODO : 유저 인증 로직 추가 +// Agenda savedMeeting = meetingService.createMeeting(meetingCreateRequest); +// MeetingCreateResponse meetingCreateResponse = MeetingCreateResponse.from(savedMeeting); +// +// return ResponseEntity.ok(ApiUtils.success(meetingCreateResponse)); +// } +// +// @GetMapping("/{id}") +// @Operation(summary = "단일 회의 조회", description = "지정된 id에 해당하는 회의를 조회한다.") +// public ResponseEntity> getTimerById(@PathVariable("id") Long meetingId) { +// Agenda meeting = meetingService.findById(meetingId); +// MeetingInfoResponse meetingInfoResponse = MeetingInfoResponse.from(meeting); +// +// return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); +// } + +} diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java new file mode 100644 index 0000000..04113df --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -0,0 +1,78 @@ +package org.dnd.timeet.agenda.domain; + + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.dnd.timeet.common.domain.AuditableEntity; +import org.dnd.timeet.meeting.domain.Meeting; +import org.hibernate.annotations.Where; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "agenda") +@AttributeOverride(name = "id", column = @Column(name = "agenda_id")) +@Where(clause = "is_deleted=false") +public class Agenda extends AuditableEntity { + + @ManyToOne(fetch = FetchType.LAZY) //외래키 + @JoinColumn(name = "meeting_id", nullable = false) + private Meeting meeting; + + @Column(nullable = false, length = 255) + private String title; + + @Enumerated(EnumType.STRING) + private AgendaType type; + + // 예상 소요 시간 + @Column(nullable = false, name = "estimated_duration") + private LocalTime estimatedDuration; + + // 연장된 총 시간 + @Column(nullable = false, name = "extended_duration") + private LocalTime extendedDuration; + + // 실제 소요 시간 + @Column(nullable = false, name = "actual_duration") + private LocalTime actualDuration; + + @Column(nullable = false, name = "order_num") + private Integer orderNum; + + @Enumerated(EnumType.STRING) + private AgendaStatus status; + + + @Builder + public Agenda(Meeting meeting, String title, AgendaType type, LocalTime estimatedDuration, + LocalTime extendedDuration, + LocalTime actualDuration, Integer orderNum, AgendaStatus status) { + this.meeting = meeting; + this.title = title; + this.type = type; + this.estimatedDuration = estimatedDuration; + this.extendedDuration = extendedDuration; + this.actualDuration = actualDuration; + this.orderNum = orderNum; + this.status = status; + } + + public void assignToMeeting(Meeting meeting) { + this.meeting = meeting; + } + +} + diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java new file mode 100644 index 0000000..8a52c56 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java @@ -0,0 +1,8 @@ +package org.dnd.timeet.agenda.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AgendaRepository extends JpaRepository { + +// Optional findByUserId(Long id); +} diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java new file mode 100644 index 0000000..9096823 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java @@ -0,0 +1,5 @@ +package org.dnd.timeet.agenda.domain; + +public enum AgendaStatus { + PENDING, IN_PROGRESS, PAUSED, COMPLETED +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaType.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaType.java new file mode 100644 index 0000000..59d57d3 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaType.java @@ -0,0 +1,5 @@ +package org.dnd.timeet.agenda.domain; + +public enum AgendaType { + AGENDA, BREAK +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java new file mode 100644 index 0000000..dbb8033 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java @@ -0,0 +1,54 @@ +//package org.dnd.timeet.agenda.dto; +// +//import io.swagger.v3.oas.annotations.media.Schema; +//import jakarta.validation.constraints.NotNull; +//import java.time.LocalDateTime; +//import java.time.LocalTime; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +//import lombok.Setter; +//import org.dnd.timeet.agenda.domain.Agenda; +//import org.springframework.format.annotation.DateTimeFormat; +// +// +//@Schema(description = "회의 생성 요청") +//@Getter +//@Setter +//@NoArgsConstructor +//public class AgendaCreateRequest { +// +// @NotNull(message = "회의 제목은 반드시 입력되어야 합니다") +// @Schema(description = "회의 제목", example = "2차 업무 회의") +// private String title; +// +// @Schema(description = "회의 장소", example = "스타벅스 강남역점") +// private String location; +// +// @NotNull(message = "회의 시작 시간은 반드시 입력되어야 합니다") +// @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") +// @Schema(description = "회의 시작 시간", example = "2024-01-11T13:20") +// private LocalDateTime startTime; +// +// @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") +// private String description; +// +// @NotNull(message = "예상 소요 시간은 반드시 입력되어야 합니다") +// @DateTimeFormat(pattern = "HH:mm") +// @Schema(description = "예상 소요 시간", example = "02:00") +// private LocalTime estimatedTotalDuration; +// +// @NotNull(message = "썸네일은 반드시 입력되어야 합니다") +// @Schema(description = "썸네일 이미지 번호", example = "1") +// private Integer imageNum; +// +// public Agenda toEntity() { +// return Agenda.builder() +// .title(this.title) +// .location(this.location) +// .startTime(startTime) +// .description(this.description) +// .totalEstimatedDuration(this.estimatedTotalDuration) +// .imgNum(imageNum) +// .build(); +// } +//} diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java new file mode 100644 index 0000000..0dbc7e3 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java @@ -0,0 +1,37 @@ +//package org.dnd.timeet.agenda.dto; +// +//import io.swagger.v3.oas.annotations.media.Schema; +//import lombok.Builder; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +//import lombok.Setter; +//import org.dnd.timeet.agenda.domain.Agenda; +// +// +//@Schema(description = "회의 생성 응답") +//@Getter +//@Setter +//@NoArgsConstructor +//public class AgendaCreateResponse { +// +// @Schema(description = "회의 id", example = "12L") +// private Long meetingId; +// +//// @Schema(description = "회의 공유 url", example = "http://localhost:8080/meetings/12L") +//// private String shareUrl; +// +// @Builder +//// public MeetingCreateResponse(Long meetingId, String shareUrl) { +// public AgendaCreateResponse(Long meetingId) { +// this.meetingId = meetingId; +//// this.shareUrl = shareUrl; +// } +// +// // 매개변수로부터 객체를 생성하는 팩토리 메서드 +// public static AgendaCreateResponse from(Agenda meeting) { +// return AgendaCreateResponse.builder() +// .meetingId(meeting.getId()) +//// .shareUrl(link.getUri().toString()) +// .build(); +// } +//} diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java new file mode 100644 index 0000000..4dfd784 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java @@ -0,0 +1,38 @@ +//package org.dnd.timeet.agenda.dto; +// +//import io.swagger.v3.oas.annotations.media.Schema; +//import lombok.Builder; +//import lombok.Getter; +//import lombok.Setter; +//import org.dnd.timeet.agenda.domain.Agenda; +// +//@Schema(description = "회의 정보 응답") +//@Getter +//@Setter +//public class AgendaInfoResponse { +// +// @Schema(description = "회의 id", example = "12L") +// private Long meetingId; +// +// @Schema(description = "회의 제목", example = "2차 회의") +// private String title; +// +// @Schema(description = "회의 목", example = "2개의 사안 모두 해결하기") +// private String description; +// +// @Builder +// public AgendaInfoResponse(Long meetingId, String title, String description) { +// this.meetingId = meetingId; +// this.title = title; +// this.description = description; +// } +// +// +// public static AgendaInfoResponse from(Agenda meeting) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 +// return AgendaInfoResponse.builder() +// .meetingId(meeting.getId()) +// .title(meeting.getTitle()) +// .description(meeting.getDescription()) +// .build(); +// } +//} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index c494332..30c4430 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -2,21 +2,26 @@ import jakarta.persistence.AttributeOverride; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.Duration; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.dnd.timeet.common.domain.AuditableEntity; import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.agenda.domain.Agenda; import org.hibernate.annotations.Where; @Entity @@ -30,17 +35,19 @@ public class Meeting extends AuditableEntity { @Column(nullable = false, length = 255) private String title; - @Column(nullable = false) + @Column(nullable = false, name = "start_time") private LocalDateTime startTime; // 종료시간, 초기값 null + @Column(name = "end_time") private LocalDateTime endTime; // 예상 소요 시간 - @Column(nullable = false) + @Column(nullable = false, name = "total_estimated_duration") private LocalTime totalEstimatedDuration; // 실제 소요 시간, 초기값 null + @Column(name = "total_actual_duration") private LocalTime totalActualDuration; @Column(nullable = true, length = 255) @@ -53,12 +60,15 @@ public class Meeting extends AuditableEntity { @Column(nullable = false, length = 50) private MeetingStatus status = MeetingStatus.SCHEDULED; - @Column(nullable = false) + @Column(nullable = false, name = "img_num") private Integer imgNum; + @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) + private List agendas = new ArrayList<>(); + @Builder public Meeting(String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, String location, - String description, Integer imgNum) { + String description, Integer imgNum) { this.title = title; this.startTime = startTime; this.totalEstimatedDuration = totalEstimatedDuration; @@ -96,5 +106,15 @@ public void updateStartTime(LocalDateTime startTime) { } this.startTime = startTime; } + + public void addAgenda(Agenda agenda) { + agendas.add(agenda); + agenda.assignToMeeting(this); + } + + public void removeAgenda(Agenda agenda) { + agendas.remove(agenda); + agenda.assignToMeeting(null); + } } diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 9d684c8..a08e640 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -29,10 +29,10 @@ public class Member extends BaseEntity { @Column(length = 100, nullable = false) private String name; - @Column(length = 255, nullable = false) + @Column(length = 255, nullable = false, name = "image_url") private String imageUrl; - @Column(length = 100, nullable = false) + @Column(length = 100, nullable = false, name = "oauth_id") private Long oauthId; @Enumerated(EnumType.STRING) diff --git a/src/main/java/org/dnd/timeet/timer/domain/Timer.java b/src/main/java/org/dnd/timeet/timer/domain/Timer.java index 2f964eb..738937c 100644 --- a/src/main/java/org/dnd/timeet/timer/domain/Timer.java +++ b/src/main/java/org/dnd/timeet/timer/domain/Timer.java @@ -27,6 +27,7 @@ public class Timer extends AuditableEntity { private TimerStatus status; @Embedded + @Column(nullable = false, name = "timer_duration") private TimerDuration timerDuration; @Builder From 2e43e19ac0752f94625947c9b8a33ce018fa7ed2 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 19:46:10 +0900 Subject: [PATCH 30/81] [#24] feat: Implement agenda creation API --- .../agenda/application/AgendaService.java | 21 ++-- .../agenda/controller/AgendaController.java | 32 ++++-- .../org/dnd/timeet/agenda/domain/Agenda.java | 66 ++++++++++-- .../timeet/agenda/domain/AgendaStatus.java | 2 +- .../agenda/dto/AgendaCreateRequest.java | 102 +++++++++--------- .../timeet/agenda/dto/AgendaInfoResponse.java | 2 +- .../dnd/timeet/meeting/domain/Meeting.java | 18 +--- 7 files changed, 143 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index d27ac20..776da7e 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -6,6 +6,9 @@ import org.dnd.timeet.agenda.domain.AgendaRepository; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.common.exception.NotFoundError.ErrorCode; +import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.meeting.domain.MeetingRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,20 +17,22 @@ @Transactional // DB 변경 작업에 사용 public class AgendaService { - private final AgendaRepository meetingRepository; + private final MeetingRepository meetingRepository; + private final AgendaRepository agendaRepository; - public Agenda createMeeting(AgendaCreateRequest createDto) { - Agenda meeting = createDto.toEntity(); - // 복잡한 비즈니스 로직은 도메인 메서드를 이용하여 Service에서 처리 - return meetingRepository.save(meeting); + public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + Agenda agenda = createDto.toEntity(meeting); + + return agendaRepository.save(agenda); } @Transactional(readOnly = true) public Agenda findById(Long id) { - return meetingRepository.findById(id) + return agendaRepository.findById(id) .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("MeetingId", "Meeting not found"))); } - - } diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index 93b2221..d648bf1 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -1,29 +1,39 @@ package org.dnd.timeet.agenda.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.dnd.timeet.agenda.application.AgendaService; +import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.agenda.dto.AgendaCreateRequest; +import org.dnd.timeet.common.utils.ApiUtils; +import org.dnd.timeet.common.utils.ApiUtils.ApiResult; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Tag(name = "안건 컨트롤러", description = "Agenda API입니다.") @RestController -@RequestMapping("/api/agendas") +@RequestMapping("/api/meetings") @RequiredArgsConstructor public class AgendaController { private final AgendaService agendaService; -// @PostMapping -// @Operation(summary = "회의 생성", description = "회의를 생성한다.") -// public ResponseEntity> createMeeting( -// @RequestBody @Valid MeetingCreateRequest meetingCreateRequest) { -// // TODO : 유저 인증 로직 추가 -// Agenda savedMeeting = meetingService.createMeeting(meetingCreateRequest); -// MeetingCreateResponse meetingCreateResponse = MeetingCreateResponse.from(savedMeeting); -// -// return ResponseEntity.ok(ApiUtils.success(meetingCreateResponse)); -// } + @PostMapping("/{meeting-id}/agendas") + @Operation(summary = "안건(+쉬는시간) 생성", description = "안건(+쉬는시간)을 생성한다.") + public ResponseEntity> createMeeting( + @PathVariable("meeting-id") Long meetingId, + @RequestBody @Valid AgendaCreateRequest agendaCreateRequest) { + // TODO : 유저 인증 로직 추가 + Agenda savedMeeting = agendaService.createAgenda(meetingId, agendaCreateRequest); + + return ResponseEntity.ok(ApiUtils.success(savedMeeting.getId())); + } // // @GetMapping("/{id}") // @Operation(summary = "단일 회의 조회", description = "지정된 id에 해당하는 회의를 조회한다.") diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 04113df..6bf3bfe 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -11,11 +11,13 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.time.LocalTime; +import java.util.Collections; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.dnd.timeet.common.domain.AuditableEntity; +import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.meeting.domain.Meeting; import org.hibernate.annotations.Where; @@ -42,30 +44,28 @@ public class Agenda extends AuditableEntity { private LocalTime estimatedDuration; // 연장된 총 시간 - @Column(nullable = false, name = "extended_duration") + @Column(nullable = true, name = "extended_duration") private LocalTime extendedDuration; // 실제 소요 시간 - @Column(nullable = false, name = "actual_duration") + @Column(nullable = true, name = "actual_duration") private LocalTime actualDuration; @Column(nullable = false, name = "order_num") private Integer orderNum; @Enumerated(EnumType.STRING) - private AgendaStatus status; + private AgendaStatus status = AgendaStatus.PENDING; @Builder - public Agenda(Meeting meeting, String title, AgendaType type, LocalTime estimatedDuration, - LocalTime extendedDuration, - LocalTime actualDuration, Integer orderNum, AgendaStatus status) { + public Agenda(Long id, Meeting meeting, String title, AgendaType type, LocalTime estimatedDuration, + Integer orderNum, AgendaStatus status) { + this.id = id; this.meeting = meeting; this.title = title; this.type = type; this.estimatedDuration = estimatedDuration; - this.extendedDuration = extendedDuration; - this.actualDuration = actualDuration; this.orderNum = orderNum; this.status = status; } @@ -74,5 +74,55 @@ public void assignToMeeting(Meeting meeting) { this.meeting = meeting; } + public void start() { + if (this.status != AgendaStatus.PENDING) { + throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("AgendaStatus", "Agenda can only be started from PENDING status")); + } + this.status = AgendaStatus.INPROGRESS; + } + + public void pause() { + if (this.status != AgendaStatus.INPROGRESS) { + throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("AgendaStatus", "Agenda can only be paused from INPROGRESS status.")); + } + this.status = AgendaStatus.PAUSED; + } + + public void resume() { + if (this.status != AgendaStatus.PAUSED) { + throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("AgendaStatus", "Agenda can only be resumed from PAUSED status.")); + } + this.status = AgendaStatus.INPROGRESS; + } + + public void extendDuration(LocalTime extension) { + if (this.extendedDuration == null) { + this.extendedDuration = extension; + } else { + this.extendedDuration = this.extendedDuration.plusHours(extension.getHour()) + .plusMinutes(extension.getMinute()); + } + } + + public void complete() { + if (this.status == AgendaStatus.COMPLETED) { + throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("AgendaStatus", "Agenda is already completed.")); + } + this.status = AgendaStatus.COMPLETED; + this.actualDuration = calculateActualDuration(); + } + + private LocalTime calculateActualDuration() { + if (this.extendedDuration != null) { + return this.estimatedDuration.plusHours(this.extendedDuration.getHour()) + .plusMinutes(this.extendedDuration.getMinute()); + } + return this.estimatedDuration; + } + } diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java index 9096823..00a0949 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java @@ -1,5 +1,5 @@ package org.dnd.timeet.agenda.domain; public enum AgendaStatus { - PENDING, IN_PROGRESS, PAUSED, COMPLETED + PENDING, INPROGRESS, PAUSED, COMPLETED } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java index dbb8033..12922ca 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java @@ -1,54 +1,48 @@ -//package org.dnd.timeet.agenda.dto; -// -//import io.swagger.v3.oas.annotations.media.Schema; -//import jakarta.validation.constraints.NotNull; -//import java.time.LocalDateTime; -//import java.time.LocalTime; -//import lombok.Getter; -//import lombok.NoArgsConstructor; -//import lombok.Setter; -//import org.dnd.timeet.agenda.domain.Agenda; -//import org.springframework.format.annotation.DateTimeFormat; -// -// -//@Schema(description = "회의 생성 요청") -//@Getter -//@Setter -//@NoArgsConstructor -//public class AgendaCreateRequest { -// -// @NotNull(message = "회의 제목은 반드시 입력되어야 합니다") -// @Schema(description = "회의 제목", example = "2차 업무 회의") -// private String title; -// -// @Schema(description = "회의 장소", example = "스타벅스 강남역점") -// private String location; -// -// @NotNull(message = "회의 시작 시간은 반드시 입력되어야 합니다") -// @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") -// @Schema(description = "회의 시작 시간", example = "2024-01-11T13:20") -// private LocalDateTime startTime; -// -// @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") -// private String description; -// -// @NotNull(message = "예상 소요 시간은 반드시 입력되어야 합니다") -// @DateTimeFormat(pattern = "HH:mm") -// @Schema(description = "예상 소요 시간", example = "02:00") -// private LocalTime estimatedTotalDuration; -// -// @NotNull(message = "썸네일은 반드시 입력되어야 합니다") -// @Schema(description = "썸네일 이미지 번호", example = "1") -// private Integer imageNum; -// -// public Agenda toEntity() { -// return Agenda.builder() -// .title(this.title) -// .location(this.location) -// .startTime(startTime) -// .description(this.description) -// .totalEstimatedDuration(this.estimatedTotalDuration) -// .imgNum(imageNum) -// .build(); -// } -//} +package org.dnd.timeet.agenda.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.time.LocalTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.agenda.domain.AgendaType; +import org.dnd.timeet.meeting.domain.Meeting; +import org.springframework.format.annotation.DateTimeFormat; + + +@Schema(description = "안건 생성 요청") +@Getter +@Setter +@NoArgsConstructor +public class AgendaCreateRequest { + + @NotNull(message = "안건 제목은 반드시 입력되어야 합니다") + @Schema(description = "안건 제목", example = "브레인스토밍") + private String title; + + @NotNull(message = "안건 타입은 반드시 입력되어야 합니다") + @Schema(description = "AGENDA | BREAK", example = "AGENDA") + private String type; + + @NotNull(message = "안건 소요 시간은 반드시 입력되어야 합니다") + @DateTimeFormat(pattern = "HH:mm") + @Schema(description = "안건 소요 시간", example = "01:20") + private LocalTime estimatedDuration; + + @NotNull(message = "안건 순서는 반드시 입력되어야 합니다") + @Schema(description = "안건 순서", example = "1") + private Integer orderNum; + + public Agenda toEntity(Meeting meeting) { + return Agenda.builder() + .meeting(meeting) + .title(this.title) + .type(this.type.equals("AGENDA") ? AgendaType.AGENDA : AgendaType.BREAK) + .estimatedDuration(this.estimatedDuration) + .orderNum(this.orderNum) + .build(); + } +} diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java index 4dfd784..5e1750e 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java @@ -17,7 +17,7 @@ // @Schema(description = "회의 제목", example = "2차 회의") // private String title; // -// @Schema(description = "회의 목", example = "2개의 사안 모두 해결하기") +// @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") // private String description; // // @Builder diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index 30c4430..dd9f385 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -19,9 +19,9 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.common.domain.AuditableEntity; import org.dnd.timeet.common.exception.BadRequestError; -import org.dnd.timeet.agenda.domain.Agenda; import org.hibernate.annotations.Where; @Entity @@ -63,9 +63,6 @@ public class Meeting extends AuditableEntity { @Column(nullable = false, name = "img_num") private Integer imgNum; - @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) - private List agendas = new ArrayList<>(); - @Builder public Meeting(String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, String location, String description, Integer imgNum) { @@ -78,12 +75,10 @@ public Meeting(String title, LocalDateTime startTime, LocalTime totalEstimatedDu } - // 회의를 시작하는 메서드 public void startMeeting() { this.status = MeetingStatus.INPROGRESS; } - // 회의를 종료하는 메서드 public void endMeeting() { this.endTime = LocalDateTime.now(); this.status = MeetingStatus.COMPLETED; @@ -92,13 +87,11 @@ public void endMeeting() { this.totalActualDuration = LocalTime.ofSecondOfDay(durationInSeconds); } - // 회의를 취소하는 메서드 public void cancelMeeting() { this.status = MeetingStatus.CANCELED; this.delete(); } - // 회의 시작 시간 수정하는 메서드 public void updateStartTime(LocalDateTime startTime) { if (this.status != MeetingStatus.SCHEDULED) { throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, @@ -107,14 +100,5 @@ public void updateStartTime(LocalDateTime startTime) { this.startTime = startTime; } - public void addAgenda(Agenda agenda) { - agendas.add(agenda); - agenda.assignToMeeting(this); - } - - public void removeAgenda(Agenda agenda) { - agendas.remove(agenda); - agenda.assignToMeeting(null); - } } From 849a53b1040be2227d2746ed87537087d249bbf7 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 22:03:32 +0900 Subject: [PATCH 31/81] [#-] feat: Add GlobalExceptionHandler --- .../exception/GlobalExceptionHandler.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/common/exception/GlobalExceptionHandler.java diff --git a/src/main/java/org/dnd/timeet/common/exception/GlobalExceptionHandler.java b/src/main/java/org/dnd/timeet/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..09f6fda --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,134 @@ +package org.dnd.timeet.common.exception; + +import java.util.HashMap; +import java.util.Map; +import org.dnd.timeet.common.utils.ApiUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ApiException.class) + public ResponseEntity> handleApiException(ApiException e) { + + return new ResponseEntity<>(e.body(), e.getStatus()); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map message = new HashMap<>(); + message.put("error", e.getMessage()); + + BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message + ); + + return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e) { + Map message = new HashMap<>(); // 맵으로 변경 + String errorMessage = String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", + e.getName(), e.getValue(), e.getRequiredType().getSimpleName()); + message.put("error", errorMessage); + + BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message + ); + + return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { + Map messages = new HashMap<>(); + ex.getBindingResult().getFieldErrors().forEach(error -> + messages.put(error.getField(), error.getDefaultMessage())); + + BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.VALIDATION_FAILED; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + messages + ); + + return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity> handleMissingParams(MissingServletRequestParameterException ex) { + Map message = new HashMap<>(); + message.put("error", String.format("The required parameter '%s' of type '%s' is missing", ex.getParameterName(), ex.getParameterType())); + + BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.MISSING_PART; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message + ); + + return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MissingServletRequestPartException.class) + public ResponseEntity> handleMissingServletRequestPartException(MissingServletRequestPartException e) { + Map message = new HashMap<>(); + message.put("error", e.getMessage()); + + BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.MISSING_PART; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message + ); + + return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> unknownServerError(Exception e) { + Map message = new HashMap<>(); + message.put("error", e.getMessage()); + + InternalServerError.ErrorCode errorCode = InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), + String.valueOf(errorCode.getCode()), + message + ); + + return new ResponseEntity<>(errorResult, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleHttpMessageNotReadable(HttpMessageNotReadableException e) { + Map message = new HashMap<>(); + message.put("error", "The request body is not readable or has an invalid format."); + + BadRequestError.ErrorCode errorCode = BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION; + ApiUtils.ApiResult errorResult = ApiUtils.error( + String.valueOf(HttpStatus.BAD_REQUEST.value()), + String.valueOf(errorCode.getCode()), + message + ); + + return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST); + } + +} + From efd4f5452031b783c46651b32868baf322be0ac1 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 22:04:36 +0900 Subject: [PATCH 32/81] [#13] feat: Implement Participant entity and repository --- .../participant/domain/Participant.java | 57 +++++++++++++++++++ .../domain/ParticipantRepository.java | 9 +++ 2 files changed, 66 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/participant/domain/Participant.java create mode 100644 src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java diff --git a/src/main/java/org/dnd/timeet/participant/domain/Participant.java b/src/main/java/org/dnd/timeet/participant/domain/Participant.java new file mode 100644 index 0000000..4783e79 --- /dev/null +++ b/src/main/java/org/dnd/timeet/participant/domain/Participant.java @@ -0,0 +1,57 @@ +package org.dnd.timeet.participant.domain; + + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.dnd.timeet.common.domain.AuditableEntity; +import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.member.domain.Member; +import org.hibernate.annotations.Where; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "participant") +@AttributeOverride(name = "id", column = @Column(name = "participant_id")) +@Where(clause = "is_deleted=false") +public class Participant extends AuditableEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meeting_id") + private Meeting meeting; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Builder + public Participant(Meeting meeting, Member member) { + this.meeting = meeting; + this.member = member; + + meeting.getParticipants().add(this); + member.getParticipations().add(this); + } + + +} + diff --git a/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java b/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java new file mode 100644 index 0000000..64a4d6c --- /dev/null +++ b/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java @@ -0,0 +1,9 @@ +package org.dnd.timeet.participant.domain; + +import org.dnd.timeet.participant.domain.Participant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ParticipantRepository extends JpaRepository { + +// Optional findByUserId(Long id); +} From 8e69a92a8c37e38e7c20cef22868d38f82e6fb9b Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 22:06:24 +0900 Subject: [PATCH 33/81] [#19] feat: Update code according to ERD and implement meeting attention api --- .../meeting/application/MeetingService.java | 56 +++++++++++++++++-- .../meeting/controller/MeetingController.java | 20 ++++++- .../dnd/timeet/meeting/domain/Meeting.java | 49 ++++++++++++++-- .../meeting/dto/MeetingCreateRequest.java | 4 +- .../org/dnd/timeet/member/domain/Member.java | 8 +++ 5 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index d076203..ca76540 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -1,15 +1,18 @@ package org.dnd.timeet.meeting.application; import java.util.Collections; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; +import org.dnd.timeet.participant.domain.Participant; +import org.dnd.timeet.participant.domain.ParticipantRepository; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; -import org.dnd.timeet.timer.domain.Timer; +import org.dnd.timeet.member.domain.Member; +import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,11 +22,42 @@ public class MeetingService { private final MeetingRepository meetingRepository; + private final ParticipantRepository participantRepository; - public Meeting createMeeting(MeetingCreateRequest createDto) { - Meeting meeting = createDto.toEntity(); - // 복잡한 비즈니스 로직은 도메인 메서드를 이용하여 Service에서 처리 - return meetingRepository.save(meeting); + public Meeting createMeeting(MeetingCreateRequest createDto, Member member) { + Meeting meeting = createDto.toEntity(member); + meeting = meetingRepository.save(meeting); + + Participant participant = new Participant(meeting, member); + participantRepository.save(participant); + + return meeting; + } + + public Meeting addParticipantToMeeting(Long meetingId, Member member) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + // 멤버가 이미 회의에 참가하고 있는지 확인 + boolean alreadyParticipating = meeting.getParticipants().stream() + .anyMatch(participant -> participant.getMember().equals(member)); + + if (alreadyParticipating) { + // 에러 메세지 발생 + throw new BadRequestError(BadRequestError.ErrorCode.DUPLICATE_RESOURCE, + Collections.singletonMap("Member", "Member already participating in the meeting")); + } + + // Participant 인스턴스 생성 및 저장 + Participant participant = new Participant(meeting, member); + participantRepository.save(participant); + + // 양방향 연관관계 설정 + meeting.getParticipants().add(participant); + member.getParticipations().add(participant); + + return meeting; } @Transactional(readOnly = true) @@ -33,5 +67,15 @@ public Meeting findById(Long id) { Collections.singletonMap("MeetingId", "Meeting not found"))); } + public void removeParticipant(Long meetingId, Member member) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + if (meeting.getHostMember().equals(member)) { + meeting.assignNewHostRandomly(); + } + + } } diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index e90cd67..32ecb2a 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.security.CustomUserDetails; import org.dnd.timeet.common.utils.ApiUtils; import org.dnd.timeet.common.utils.ApiUtils.ApiResult; import org.dnd.timeet.meeting.application.MeetingService; @@ -12,6 +13,7 @@ import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -30,9 +32,21 @@ public class MeetingController { @PostMapping @Operation(summary = "회의 생성", description = "회의를 생성한다.") public ResponseEntity> createMeeting( - @RequestBody @Valid MeetingCreateRequest meetingCreateRequest) { - // TODO : 유저 인증 로직 추가 - Meeting savedMeeting = meetingService.createMeeting(meetingCreateRequest); + @RequestBody @Valid MeetingCreateRequest meetingCreateRequest, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Meeting savedMeeting = meetingService.createMeeting(meetingCreateRequest, userDetails.getMember()); + MeetingCreateResponse meetingCreateResponse = MeetingCreateResponse.from(savedMeeting); + + return ResponseEntity.ok(ApiUtils.success(meetingCreateResponse)); + } + + @PostMapping("/{meeting-id}/attend") + @Operation(summary = "회의 참가", description = "회의에 참가한다.") + public ResponseEntity> attendMeeting( + @PathVariable("meeting-id") Long meetingId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + Meeting savedMeeting = meetingService.addParticipantToMeeting(meetingId, userDetails.getMember()); MeetingCreateResponse meetingCreateResponse = MeetingCreateResponse.from(savedMeeting); return ResponseEntity.ok(ApiUtils.success(meetingCreateResponse)); diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index dd9f385..32cde56 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -2,26 +2,32 @@ import jakarta.persistence.AttributeOverride; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.Duration; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.common.domain.AuditableEntity; import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.participant.domain.Participant; import org.hibernate.annotations.Where; @Entity @@ -32,6 +38,10 @@ @Where(clause = "is_deleted=false") public class Meeting extends AuditableEntity { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "host_member_id") + private Member hostMember; // 방장 + @Column(nullable = false, length = 255) private String title; @@ -63,9 +73,13 @@ public class Meeting extends AuditableEntity { @Column(nullable = false, name = "img_num") private Integer imgNum; + @OneToMany(mappedBy = "meeting", fetch = FetchType.EAGER) + private Set participants = new HashSet<>(); + @Builder - public Meeting(String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, String location, - String description, Integer imgNum) { + public Meeting(Member hostMember, String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, + String location, String description, Integer imgNum) { + this.hostMember = hostMember; this.title = title; this.startTime = startTime; this.totalEstimatedDuration = totalEstimatedDuration; @@ -100,5 +114,30 @@ public void updateStartTime(LocalDateTime startTime) { this.startTime = startTime; } + public void assignHostMember(Member hostMember) { + this.hostMember = hostMember; + } + + // 새 방장을 랜덤으로 지정하는 메서드 + public void assignNewHostRandomly() { + if (this.participants.isEmpty()) { + this.hostMember = null; // 참가자가 없을 경우 방장도 없음 + return; + } + + List participantsList = this.participants.stream() + .map(Participant::getMember) + .collect(Collectors.toList()); + + // 랜덤 객체를 사용하여 참가자 목록에서 랜덤하게 하나를 선택 + Random random = new Random(); + int index = random.nextInt(participantsList.size()); + Member newHost = participantsList.get(index); + + // 새로운 방장 지정 + this.assignHostMember(newHost); + } + + } diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java index 8470e6f..fa41bc5 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java @@ -9,6 +9,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.member.domain.Member; import org.springframework.format.annotation.DateTimeFormat; @@ -42,8 +43,9 @@ public class MeetingCreateRequest { @Schema(description = "썸네일 이미지 번호", example = "1") private Integer imageNum; - public Meeting toEntity() { + public Meeting toEntity(Member member) { return Meeting.builder() + .hostMember(member) .title(this.title) .location(this.location) .startTime(startTime) diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index a08e640..a8b49cc 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -5,13 +5,18 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.util.HashSet; +import java.util.Set; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.dnd.timeet.common.domain.BaseEntity; import org.dnd.timeet.oauth.OAuth2Provider; +import org.dnd.timeet.participant.domain.Participant; import org.hibernate.annotations.Where; @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -39,6 +44,9 @@ public class Member extends BaseEntity { @Column(length = 50, nullable = false) private OAuth2Provider provider; + @OneToMany(mappedBy = "member", fetch = FetchType.EAGER) + private Set participations = new HashSet<>(); + // MEMO : 필수값들이므로 final 붙임 @Builder public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth2Provider provider) { From ffe4a48f38df4236670576a72996da873ec54ae6 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Fri, 9 Feb 2024 22:40:18 +0900 Subject: [PATCH 34/81] [#24] feat: Implement agenda retreival API --- .../agenda/application/AgendaService.java | 7 +- .../agenda/controller/AgendaController.java | 26 ++++-- .../org/dnd/timeet/agenda/domain/Agenda.java | 4 - .../agenda/domain/AgendaRepository.java | 3 +- .../agenda/dto/AgendaCreateRequest.java | 2 + .../agenda/dto/AgendaCreateResponse.java | 37 -------- .../timeet/agenda/dto/AgendaInfoResponse.java | 92 +++++++++++-------- 7 files changed, 77 insertions(+), 94 deletions(-) delete mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index 776da7e..398883d 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -1,6 +1,7 @@ package org.dnd.timeet.agenda.application; import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.domain.AgendaRepository; @@ -30,9 +31,7 @@ public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto) { } @Transactional(readOnly = true) - public Agenda findById(Long id) { - return agendaRepository.findById(id) - .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("MeetingId", "Meeting not found"))); + public List findAll(Long meetingId) { + return agendaRepository.findByMeetingId(meetingId); } } diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index d648bf1..2fbbf71 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -3,13 +3,17 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.dnd.timeet.agenda.application.AgendaService; import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; +import org.dnd.timeet.agenda.dto.AgendaInfoResponse; import org.dnd.timeet.common.utils.ApiUtils; import org.dnd.timeet.common.utils.ApiUtils.ApiResult; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -29,19 +33,21 @@ public class AgendaController { public ResponseEntity> createMeeting( @PathVariable("meeting-id") Long meetingId, @RequestBody @Valid AgendaCreateRequest agendaCreateRequest) { - // TODO : 유저 인증 로직 추가 Agenda savedMeeting = agendaService.createAgenda(meetingId, agendaCreateRequest); return ResponseEntity.ok(ApiUtils.success(savedMeeting.getId())); } -// -// @GetMapping("/{id}") -// @Operation(summary = "단일 회의 조회", description = "지정된 id에 해당하는 회의를 조회한다.") -// public ResponseEntity> getTimerById(@PathVariable("id") Long meetingId) { -// Agenda meeting = meetingService.findById(meetingId); -// MeetingInfoResponse meetingInfoResponse = MeetingInfoResponse.from(meeting); -// -// return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); -// } + + @GetMapping("/{meeting-id}/agendas") + @Operation(summary = "모든 안건 조회", description = "모든 안건을 조회한다.") + public ResponseEntity getAgendas( + @PathVariable("meeting-id") Long meetingId) { + List agendaInfoResponseList = agendaService.findAll(meetingId) + .stream() + .map(AgendaInfoResponse::from) + .collect(Collectors.toList()); + + return ResponseEntity.ok(ApiUtils.success(agendaInfoResponseList)); + } } diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 6bf3bfe..4988184 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -70,10 +70,6 @@ public Agenda(Long id, Meeting meeting, String title, AgendaType type, LocalTime this.status = status; } - public void assignToMeeting(Meeting meeting) { - this.meeting = meeting; - } - public void start() { if (this.status != AgendaStatus.PENDING) { throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java index 8a52c56..e2707c0 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java @@ -1,8 +1,9 @@ package org.dnd.timeet.agenda.domain; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface AgendaRepository extends JpaRepository { -// Optional findByUserId(Long id); + List findByMeetingId(Long meetingId); } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java index 12922ca..c23b4fc 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.agenda.domain.AgendaStatus; import org.dnd.timeet.agenda.domain.AgendaType; import org.dnd.timeet.meeting.domain.Meeting; import org.springframework.format.annotation.DateTimeFormat; @@ -43,6 +44,7 @@ public Agenda toEntity(Meeting meeting) { .type(this.type.equals("AGENDA") ? AgendaType.AGENDA : AgendaType.BREAK) .estimatedDuration(this.estimatedDuration) .orderNum(this.orderNum) + .status(AgendaStatus.PENDING) .build(); } } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java deleted file mode 100644 index 0dbc7e3..0000000 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -//package org.dnd.timeet.agenda.dto; -// -//import io.swagger.v3.oas.annotations.media.Schema; -//import lombok.Builder; -//import lombok.Getter; -//import lombok.NoArgsConstructor; -//import lombok.Setter; -//import org.dnd.timeet.agenda.domain.Agenda; -// -// -//@Schema(description = "회의 생성 응답") -//@Getter -//@Setter -//@NoArgsConstructor -//public class AgendaCreateResponse { -// -// @Schema(description = "회의 id", example = "12L") -// private Long meetingId; -// -//// @Schema(description = "회의 공유 url", example = "http://localhost:8080/meetings/12L") -//// private String shareUrl; -// -// @Builder -//// public MeetingCreateResponse(Long meetingId, String shareUrl) { -// public AgendaCreateResponse(Long meetingId) { -// this.meetingId = meetingId; -//// this.shareUrl = shareUrl; -// } -// -// // 매개변수로부터 객체를 생성하는 팩토리 메서드 -// public static AgendaCreateResponse from(Agenda meeting) { -// return AgendaCreateResponse.builder() -// .meetingId(meeting.getId()) -//// .shareUrl(link.getUri().toString()) -// .build(); -// } -//} diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java index 5e1750e..03fa66a 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java @@ -1,38 +1,54 @@ -//package org.dnd.timeet.agenda.dto; -// -//import io.swagger.v3.oas.annotations.media.Schema; -//import lombok.Builder; -//import lombok.Getter; -//import lombok.Setter; -//import org.dnd.timeet.agenda.domain.Agenda; -// -//@Schema(description = "회의 정보 응답") -//@Getter -//@Setter -//public class AgendaInfoResponse { -// -// @Schema(description = "회의 id", example = "12L") -// private Long meetingId; -// -// @Schema(description = "회의 제목", example = "2차 회의") -// private String title; -// -// @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") -// private String description; -// -// @Builder -// public AgendaInfoResponse(Long meetingId, String title, String description) { -// this.meetingId = meetingId; -// this.title = title; -// this.description = description; -// } -// -// -// public static AgendaInfoResponse from(Agenda meeting) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 -// return AgendaInfoResponse.builder() -// .meetingId(meeting.getId()) -// .title(meeting.getTitle()) -// .description(meeting.getDescription()) -// .build(); -// } -//} \ No newline at end of file +package org.dnd.timeet.agenda.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalTime; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.dnd.timeet.agenda.domain.Agenda; + +@Schema(description = "회의 정보 응답") +@Getter +@Setter +public class AgendaInfoResponse { + + @Schema(description = "안건 id", example = "12") + private Long agendaId; + + @Schema(description = "안건 제목", example = "안건1") + private String title; + + @Schema(description = "안건 종류", example = "AGENDA") + private String type; + + @Schema(description = "예상 소요시간", example = "01:00") + private LocalTime estimatedDuration; + + @Schema(description = "실제 소요시간", example = "01:30") + private LocalTime actualDuration; + + @Schema(description = "안건 상태", example = "INPROGRESS") + private String status; + + @Builder + public AgendaInfoResponse(Long agendaId, String title, String type, LocalTime estimatedDuration, LocalTime actualDuration, String status) { + this.agendaId = agendaId; + this.title = title; + this.type = type; + this.estimatedDuration = estimatedDuration; + this.actualDuration = actualDuration; + this.status = status; + } + + + public static AgendaInfoResponse from(Agenda agenda) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 + return AgendaInfoResponse.builder() + .agendaId(agenda.getId()) + .title(agenda.getTitle()) + .type(agenda.getType().name()) + .estimatedDuration(agenda.getEstimatedDuration()) + .actualDuration(agenda.getActualDuration()) + .status(agenda.getStatus().name()) + .build(); + } +} \ No newline at end of file From d853121c3a1a6b15f759c25bb217d8745ecce690 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sat, 10 Feb 2024 14:32:50 +0900 Subject: [PATCH 35/81] [#24] refactor: Validate participant for agenda creation --- .../dnd/timeet/agenda/application/AgendaService.java | 10 +++++++++- .../dnd/timeet/agenda/controller/AgendaController.java | 7 +++++-- .../participant/domain/ParticipantRepository.java | 3 +++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index 398883d..15f5fa5 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -6,10 +6,13 @@ import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.domain.AgendaRepository; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; +import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; import org.dnd.timeet.common.exception.NotFoundError.ErrorCode; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.participant.domain.ParticipantRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,11 +23,16 @@ public class AgendaService { private final MeetingRepository meetingRepository; private final AgendaRepository agendaRepository; + private final ParticipantRepository participantRepository; - public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto) { + public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto, Member member) { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("MeetingId", "Meeting not found"))); + participantRepository.findByMeetingIdAndMemberId(meetingId, member.getId()) + .orElseThrow(() -> new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED, + Collections.singletonMap("MemberId", "Member is not a participant of the meeting"))); + Agenda agenda = createDto.toEntity(meeting); return agendaRepository.save(agenda); diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index 2fbbf71..d887d03 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -10,9 +10,11 @@ import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; import org.dnd.timeet.agenda.dto.AgendaInfoResponse; +import org.dnd.timeet.common.security.CustomUserDetails; import org.dnd.timeet.common.utils.ApiUtils; import org.dnd.timeet.common.utils.ApiUtils.ApiResult; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -32,8 +34,9 @@ public class AgendaController { @Operation(summary = "안건(+쉬는시간) 생성", description = "안건(+쉬는시간)을 생성한다.") public ResponseEntity> createMeeting( @PathVariable("meeting-id") Long meetingId, - @RequestBody @Valid AgendaCreateRequest agendaCreateRequest) { - Agenda savedMeeting = agendaService.createAgenda(meetingId, agendaCreateRequest); + @RequestBody @Valid AgendaCreateRequest agendaCreateRequest, + @AuthenticationPrincipal CustomUserDetails userDetails) { + Agenda savedMeeting = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember()); return ResponseEntity.ok(ApiUtils.success(savedMeeting.getId())); } diff --git a/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java b/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java index 64a4d6c..dce4b93 100644 --- a/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java +++ b/src/main/java/org/dnd/timeet/participant/domain/ParticipantRepository.java @@ -1,9 +1,12 @@ package org.dnd.timeet.participant.domain; +import java.util.Optional; import org.dnd.timeet.participant.domain.Participant; import org.springframework.data.jpa.repository.JpaRepository; public interface ParticipantRepository extends JpaRepository { + Optional findByMeetingIdAndMemberId(Long meetingId, Long memberId); + // Optional findByUserId(Long id); } From 2302d44008b6e0eb4e628d5bf01204803ee33b26 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sat, 10 Feb 2024 15:04:38 +0900 Subject: [PATCH 36/81] [#24] refactor: Change variable name --- .../org/dnd/timeet/agenda/controller/AgendaController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index d887d03..ec452f9 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -36,9 +36,9 @@ public ResponseEntity> createMeeting( @PathVariable("meeting-id") Long meetingId, @RequestBody @Valid AgendaCreateRequest agendaCreateRequest, @AuthenticationPrincipal CustomUserDetails userDetails) { - Agenda savedMeeting = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember()); + Agenda savedAgenda = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember()); - return ResponseEntity.ok(ApiUtils.success(savedMeeting.getId())); + return ResponseEntity.ok(ApiUtils.success(savedAgenda.getId())); } @GetMapping("/{meeting-id}/agendas") From e15185bd9323c7073fc14af58185e55f6fa3327b Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Sat, 10 Feb 2024 21:38:42 +0900 Subject: [PATCH 37/81] [#24] feat: Implement real-time agenda control and update API --- build.gradle | 1 + .../agenda/application/AgendaService.java | 30 ++++++++ .../agenda/controller/AgendaController.java | 25 ++++++ .../org/dnd/timeet/agenda/domain/Agenda.java | 2 +- .../agenda/domain/AgendaRepository.java | 3 + .../agenda/dto/AgendaActionRequest.java | 16 ++++ .../agenda/dto/AgendaActionResponse.java | 21 +++++ .../org/dnd/timeet/config/SecurityConfig.java | 76 ++++++++++--------- .../dnd/timeet/config/WebSocketConfig.java | 26 +++++++ .../member/controller/MemberController.java | 2 +- 10 files changed, 163 insertions(+), 39 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaActionRequest.java create mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java create mode 100644 src/main/java/org/dnd/timeet/config/WebSocketConfig.java diff --git a/build.gradle b/build.gradle index 33ba110..234dda6 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' implementation("org.springframework.boot:spring-boot-devtools") implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-websocket' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index 15f5fa5..9f1b28e 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -1,10 +1,12 @@ package org.dnd.timeet.agenda.application; +import java.time.LocalTime; import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.domain.AgendaRepository; +import org.dnd.timeet.agenda.dto.AgendaActionRequest; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; @@ -13,6 +15,7 @@ import org.dnd.timeet.meeting.domain.MeetingRepository; import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.participant.domain.ParticipantRepository; +import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,4 +45,31 @@ public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto, Member public List findAll(Long meetingId) { return agendaRepository.findByMeetingId(meetingId); } + + public Agenda changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequest actionRequest) { + Agenda agenda = agendaRepository.findByIdAndMeetingId(agendaId, meetingId) + .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("AgendaId", "Agenda not found"))); + + switch (actionRequest.getAction()) { + case "pause": + agenda.pause(); + break; + case "resume": + agenda.resume(); + break; + case "end": + agenda.complete(); + break; + case "modify": + LocalTime modifiedDuration = LocalTime.parse(actionRequest.getModifiedDuration()); + agenda.extendDuration(modifiedDuration); + break; + default: + throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED, + Collections.singletonMap("Action", "Invalid action")); + } + + return agendaRepository.save(agenda); // 변경된 안건 상태로 응답 객체 생성 및 반환 + } } diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index ec452f9..dffb6b6 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -8,12 +8,17 @@ import lombok.RequiredArgsConstructor; import org.dnd.timeet.agenda.application.AgendaService; import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.agenda.dto.AgendaActionRequest; +import org.dnd.timeet.agenda.dto.AgendaActionResponse; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; import org.dnd.timeet.agenda.dto.AgendaInfoResponse; import org.dnd.timeet.common.security.CustomUserDetails; import org.dnd.timeet.common.utils.ApiUtils; import org.dnd.timeet.common.utils.ApiUtils.ApiResult; import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -53,4 +58,24 @@ public ResponseEntity getAgendas( return ResponseEntity.ok(ApiUtils.success(agendaInfoResponseList)); } + /* + * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. + * @SendTo : 브로커에게 메세지를 보낸다. + * 과정 + * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. + * 2. 핸들러가 메세지를 처리한다. + * 3. 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. + * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. + */ + @Operation(summary = "안건 제어 및 갱신", description = "해당 안건을 제어 및 갱신한다.") + @MessageMapping("/meeting/{meeting-id}/agendas/{agenda-id}/action") + @SendTo("/topic/meeting/{meeting-id}/agendas/{agenda-id}/status") + public AgendaActionResponse handleAgendaAction(@DestinationVariable Long meetingId, + @DestinationVariable Long agendaId, + AgendaActionRequest actionRequest) { + // 로직 구현 (안건 상태 변경) + Agenda agenda = agendaService.changeAgendaStatus(meetingId, agendaId, actionRequest); + // 변경된 안건 상태로 응답 객체 생성 및 반환 + return new AgendaActionResponse(agenda.getId(), agenda.getStatus()); + } } diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 4988184..2663530 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -109,7 +109,7 @@ public void complete() { Collections.singletonMap("AgendaStatus", "Agenda is already completed.")); } this.status = AgendaStatus.COMPLETED; - this.actualDuration = calculateActualDuration(); + this.actualDuration = calculateActualDuration(); // 실제 소요 시간 계산 } private LocalTime calculateActualDuration() { diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java index e2707c0..8d08a36 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaRepository.java @@ -1,9 +1,12 @@ package org.dnd.timeet.agenda.domain; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface AgendaRepository extends JpaRepository { List findByMeetingId(Long meetingId); + + Optional findByIdAndMeetingId(Long agendaId, Long meetingId); } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionRequest.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionRequest.java new file mode 100644 index 0000000..1051f34 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionRequest.java @@ -0,0 +1,16 @@ +package org.dnd.timeet.agenda.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Schema(description = "안건 제어 요청") +@Getter +@Setter +@NoArgsConstructor +public class AgendaActionRequest { + private String action; + private String modifiedDuration; // "HH:MM" 형식, optional + +} diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java new file mode 100644 index 0000000..3e43b8e --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java @@ -0,0 +1,21 @@ +package org.dnd.timeet.agenda.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.dnd.timeet.agenda.domain.AgendaStatus; + +@Schema(description = "안건 제어 응답") +@Getter +@Setter +public class AgendaActionResponse { + private Long agendaId; + private AgendaStatus status; + + public AgendaActionResponse(Long agendaId, AgendaStatus status) { + this.agendaId = agendaId; + this.status = status; + } + +} diff --git a/src/main/java/org/dnd/timeet/config/SecurityConfig.java b/src/main/java/org/dnd/timeet/config/SecurityConfig.java index 1ab7bdf..99dc492 100644 --- a/src/main/java/org/dnd/timeet/config/SecurityConfig.java +++ b/src/main/java/org/dnd/timeet/config/SecurityConfig.java @@ -62,10 +62,13 @@ public class SecurityConfig { "/swagger-ui/**", "/swagger-resources/**", // open url - "/api/v1/users/**", + "/api/members/**", //h2-console "/h2-console/**", + // oauth2 "oauth2/**", + // websocket + "/ws/**" }; @Bean @@ -96,7 +99,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication ); // cors 재설정 - http.cors(cors -> cors.configurationSource(configurationSource())); + http.cors(cors -> cors.configurationSource(configurationSource())); // 개발 환경용 // jSessionId 사용 거부 (토큰 인증 방식 사용) http.sessionManagement(session -> session @@ -128,7 +131,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication http.authorizeHttpRequests(auth -> auth .requestMatchers(PUBLIC_URLS).permitAll() // 인증 없이 접근 허용 -// .anyRequest().authenticated() .anyRequest().authenticated() ); @@ -171,38 +173,38 @@ public CorsConfigurationSource configurationSource() { return source; } - // 개발 환경용 CORS 설정 - @Bean - @Profile("!prod") - public CorsConfigurationSource devCorsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedHeader("*"); - configuration.addAllowedMethod("*"); - configuration.addAllowedOrigin(frontlocalurl); - configuration.setAllowCredentials(true); - configuration.addExposedHeader("Authorization"); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } - - // 운영 환경용 CORS 설정 - @Bean - @Profile("prod") - public CorsConfigurationSource prodCorsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedHeader("*"); - configuration.addAllowedMethod("*"); - // USER, OWNER 배포 주소 (React) - configuration.addAllowedOriginPattern(prodfronturl); - configuration.setAllowCredentials(true); - configuration.addExposedHeader("Authorization"); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } +// // 개발 환경용 CORS 설정 +// @Bean +// @Profile("!prod") +// public CorsConfigurationSource devCorsConfigurationSource() { +// CorsConfiguration configuration = new CorsConfiguration(); +// configuration.addAllowedHeader("*"); +// configuration.addAllowedMethod("*"); +// configuration.addAllowedOrigin(frontlocalurl); +// configuration.setAllowCredentials(true); +// configuration.addExposedHeader("Authorization"); +// +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// source.registerCorsConfiguration("/**", configuration); +// +// return source; +// } +// +// // 운영 환경용 CORS 설정 +// @Bean +// @Profile("prod") +// public CorsConfigurationSource prodCorsConfigurationSource() { +// CorsConfiguration configuration = new CorsConfiguration(); +// configuration.addAllowedHeader("*"); +// configuration.addAllowedMethod("*"); +// // USER, OWNER 배포 주소 (React) +// configuration.addAllowedOriginPattern(prodfronturl); +// configuration.setAllowCredentials(true); +// configuration.addExposedHeader("Authorization"); +// +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// source.registerCorsConfiguration("/**", configuration); +// +// return source; +// } } diff --git a/src/main/java/org/dnd/timeet/config/WebSocketConfig.java b/src/main/java/org/dnd/timeet/config/WebSocketConfig.java new file mode 100644 index 0000000..4f0b6a9 --- /dev/null +++ b/src/main/java/org/dnd/timeet/config/WebSocketConfig.java @@ -0,0 +1,26 @@ +package org.dnd.timeet.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + // 메시지 브로커 설정 + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic"); // 메시지 브로커가 /topic으로 시작하는 메시지를 클라이언트로 브로드캐스팅 (1:N 통신) + config.setApplicationDestinationPrefixes("/app"); // 핸들러 메소드가 /app으로 시작하는 메시지를 처리 + } + + // 웹소켓 연결을 위한 endpoint 설정 + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws").withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결 + } +} + diff --git a/src/main/java/org/dnd/timeet/member/controller/MemberController.java b/src/main/java/org/dnd/timeet/member/controller/MemberController.java index 6065921..77cd84c 100644 --- a/src/main/java/org/dnd/timeet/member/controller/MemberController.java +++ b/src/main/java/org/dnd/timeet/member/controller/MemberController.java @@ -9,7 +9,7 @@ @Tag(name = "Member 컨트롤러", description = "Member API입니다.") @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/members") +@RequestMapping("/api/members") public class MemberController { private final MemberService memberService; From ffcd451fa0c8949cf8fe3d04a047a1bc8217112a Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:28:55 +0900 Subject: [PATCH 38/81] [#22] feat: Add upsertFcmToken --- .../timeet/member/application/MemberService.java | 15 +++++++++++++++ .../java/org/dnd/timeet/member/domain/Member.java | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/org/dnd/timeet/member/application/MemberService.java b/src/main/java/org/dnd/timeet/member/application/MemberService.java index 0fd9236..5912a34 100644 --- a/src/main/java/org/dnd/timeet/member/application/MemberService.java +++ b/src/main/java/org/dnd/timeet/member/application/MemberService.java @@ -1,7 +1,11 @@ package org.dnd.timeet.member.application; +import java.util.Collections; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.member.domain.MemberRepository; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -14,4 +18,15 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; + + public void upsertFcmToken(Long id, String fcmToken) { + Optional member = memberRepository.findById(id); + if (member.isPresent()) { + member.get().setFcmToken(fcmToken); + } else { + throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found")); + } + + } } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 1783892..557176d 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -51,4 +51,8 @@ public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth this.oauthId = oauthId; this.provider = provider; } + + public void setFcmToken(String fcmToken) { + this.fcmToken = fcmToken; + } } From b3454eadee27bc8c41aadf21ef7c10cea858af67 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:34:27 +0900 Subject: [PATCH 39/81] [#22] feat: Update build workflow for firebase config --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcdb8a3..2626605 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: Build -on: [pull_request, workflow_dispatch] +on: [ pull_request, workflow_dispatch ] jobs: @@ -17,6 +17,10 @@ jobs: java-version: 17 - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest + - name: Make firebase-adminsdk.json + run: | + touch src/main/resources/timeet-firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/firebase-adminsdk.json - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml From 78e45d9d6e31d6cefd823fca3030265655d41238 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:34:46 +0900 Subject: [PATCH 40/81] [#22] feat: Update build workflow for firebase config --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2626605..777fef6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - name: Make firebase-adminsdk.json run: | touch src/main/resources/timeet-firebase-adminsdk.json - echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/timeet-firebase-adminsdk.json - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml From 662ecf5a97a157884dd2191a55b5b938a8b521a4 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:39:45 +0900 Subject: [PATCH 41/81] [#22] feat: Update build workflow for firebase config --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 777fef6..7ba5d31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - name: Make firebase-adminsdk.json run: | touch src/main/resources/timeet-firebase-adminsdk.json - echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/timeet-firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" | base64 --decode > src/main/resources/timeet-firebase-adminsdk.json - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml From 0e2984318a37857843e8722944318b4ab998a3fd Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Mon, 12 Feb 2024 01:01:30 +0900 Subject: [PATCH 42/81] [#24] fix: Fix real-time socket API connection errors --- .../agenda/application/AgendaService.java | 3 +++ .../agenda/controller/AgendaController.java | 22 +++++++++---------- .../org/dnd/timeet/agenda/domain/Agenda.java | 2 +- .../timeet/agenda/dto/AgendaInfoResponse.java | 7 +++++- .../dnd/timeet/config/WebSocketConfig.java | 4 +++- .../dnd/timeet/TimeetApplicationTests.java | 2 +- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index 9f1b28e..b7592db 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -52,6 +52,9 @@ public Agenda changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequ Collections.singletonMap("AgendaId", "Agenda not found"))); switch (actionRequest.getAction()) { + case "start": + agenda.start(); + break; case "pause": agenda.pause(); break; diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index dffb6b6..555d44f 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -58,20 +58,20 @@ public ResponseEntity getAgendas( return ResponseEntity.ok(ApiUtils.success(agendaInfoResponseList)); } - /* - * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. - * @SendTo : 브로커에게 메세지를 보낸다. - * 과정 - * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. - * 2. 핸들러가 메세지를 처리한다. - * 3. 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. - * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. - */ +// /* +// * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. +// * @SendTo : 브로커에게 메세지를 보낸다. +// * 과정 +// * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. +// * 2. 핸들러가 메세지를 처리한다. +// * 3. 서버는 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. +// * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. +// */ @Operation(summary = "안건 제어 및 갱신", description = "해당 안건을 제어 및 갱신한다.") @MessageMapping("/meeting/{meeting-id}/agendas/{agenda-id}/action") @SendTo("/topic/meeting/{meeting-id}/agendas/{agenda-id}/status") - public AgendaActionResponse handleAgendaAction(@DestinationVariable Long meetingId, - @DestinationVariable Long agendaId, + public AgendaActionResponse handleAgendaAction(@DestinationVariable("meeting-id") Long meetingId, + @DestinationVariable("agenda-id") Long agendaId, AgendaActionRequest actionRequest) { // 로직 구현 (안건 상태 변경) Agenda agenda = agendaService.changeAgendaStatus(meetingId, agendaId, actionRequest); diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 2663530..eddc360 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -87,7 +87,7 @@ public void pause() { } public void resume() { - if (this.status != AgendaStatus.PAUSED) { + if (this.status != AgendaStatus.PAUSED || this.status != AgendaStatus.COMPLETED){ throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, Collections.singletonMap("AgendaStatus", "Agenda can only be resumed from PAUSED status.")); } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java index 03fa66a..3aca973 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java @@ -24,6 +24,9 @@ public class AgendaInfoResponse { @Schema(description = "예상 소요시간", example = "01:00") private LocalTime estimatedDuration; + @Schema(description = "연장된 총 시간", example = "00:30") + private LocalTime extendedDuration; + @Schema(description = "실제 소요시간", example = "01:30") private LocalTime actualDuration; @@ -31,11 +34,12 @@ public class AgendaInfoResponse { private String status; @Builder - public AgendaInfoResponse(Long agendaId, String title, String type, LocalTime estimatedDuration, LocalTime actualDuration, String status) { + public AgendaInfoResponse(Long agendaId, String title, String type, LocalTime estimatedDuration, LocalTime extendedDuration, LocalTime actualDuration, String status) { this.agendaId = agendaId; this.title = title; this.type = type; this.estimatedDuration = estimatedDuration; + this.extendedDuration = extendedDuration; this.actualDuration = actualDuration; this.status = status; } @@ -47,6 +51,7 @@ public static AgendaInfoResponse from(Agenda agenda) { // 매개변수로부터 .title(agenda.getTitle()) .type(agenda.getType().name()) .estimatedDuration(agenda.getEstimatedDuration()) + .extendedDuration(agenda.getExtendedDuration()) .actualDuration(agenda.getActualDuration()) .status(agenda.getStatus().name()) .build(); diff --git a/src/main/java/org/dnd/timeet/config/WebSocketConfig.java b/src/main/java/org/dnd/timeet/config/WebSocketConfig.java index 4f0b6a9..b9a2596 100644 --- a/src/main/java/org/dnd/timeet/config/WebSocketConfig.java +++ b/src/main/java/org/dnd/timeet/config/WebSocketConfig.java @@ -20,7 +20,9 @@ public void configureMessageBroker(MessageBrokerRegistry config) { // 웹소켓 연결을 위한 endpoint 설정 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws").withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결 + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*"); // 모든 도메인에서 접근 허용 +// .withSockJS(); // /ws로 접속하면 SockJS를 통해 웹소켓 연결 } } diff --git a/src/test/java/org/dnd/timeet/TimeetApplicationTests.java b/src/test/java/org/dnd/timeet/TimeetApplicationTests.java index eaa3d23..0444970 100644 --- a/src/test/java/org/dnd/timeet/TimeetApplicationTests.java +++ b/src/test/java/org/dnd/timeet/TimeetApplicationTests.java @@ -5,7 +5,7 @@ import org.springframework.test.context.TestPropertySource; @SpringBootTest -@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) class TimeetApplicationTests { @Test From 4c7eca029bc93011a18e0eec30567e1af3faec2c Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Mon, 12 Feb 2024 01:04:56 +0900 Subject: [PATCH 43/81] [#24] fix: Fix agenda control API logic error --- .../agenda/controller/AgendaController.java | 18 +++++++++--------- .../org/dnd/timeet/agenda/domain/Agenda.java | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index 555d44f..d770305 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -58,15 +58,15 @@ public ResponseEntity getAgendas( return ResponseEntity.ok(ApiUtils.success(agendaInfoResponseList)); } -// /* -// * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. -// * @SendTo : 브로커에게 메세지를 보낸다. -// * 과정 -// * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. -// * 2. 핸들러가 메세지를 처리한다. -// * 3. 서버는 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. -// * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. -// */ + /* + * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. + * @SendTo : 브로커에게 메세지를 보낸다. + * 과정 + * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. + * 2. 핸들러가 메세지를 처리한다. + * 3. 서버는 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. + * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. + */ @Operation(summary = "안건 제어 및 갱신", description = "해당 안건을 제어 및 갱신한다.") @MessageMapping("/meeting/{meeting-id}/agendas/{agenda-id}/action") @SendTo("/topic/meeting/{meeting-id}/agendas/{agenda-id}/status") diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index eddc360..2663530 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -87,7 +87,7 @@ public void pause() { } public void resume() { - if (this.status != AgendaStatus.PAUSED || this.status != AgendaStatus.COMPLETED){ + if (this.status != AgendaStatus.PAUSED) { throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, Collections.singletonMap("AgendaStatus", "Agenda can only be resumed from PAUSED status.")); } From c8b0e081f3b5d8f691b5bf4daa39dcc790af123c Mon Sep 17 00:00:00 2001 From: FacerAin Date: Mon, 12 Feb 2024 19:48:45 +0900 Subject: [PATCH 44/81] [#-] chore: fix typo in MeetingInfoResponse --- .../java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java index 11b2730..39e9080 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java @@ -5,8 +5,6 @@ import lombok.Getter; import lombok.Setter; import org.dnd.timeet.meeting.domain.Meeting; -import org.dnd.timeet.timer.domain.Timer; -import org.dnd.timeet.timer.domain.TimerStatus; @Schema(description = "회의 정보 응답") @Getter @@ -19,7 +17,7 @@ public class MeetingInfoResponse { @Schema(description = "회의 제목", example = "2차 회의") private String title; - @Schema(description = "회의 목", example = "2개의 사안 모두 해결하기") + @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") private String description; @Builder From e4b31b8e537b55d7aa5132307d520ab29969351f Mon Sep 17 00:00:00 2001 From: FacerAin Date: Mon, 12 Feb 2024 19:51:25 +0900 Subject: [PATCH 45/81] [#28] feat: Add getMeetingMembers --- .../meeting/application/MeetingService.java | 20 ++++++++++++++----- .../meeting/controller/MeetingController.java | 15 ++++++++++++++ .../meeting/domain/MeetingRepository.java | 7 ++++++- .../timeet/member/dto/MemberInfoResponse.java | 13 ++++-------- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index ca76540..acec501 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -1,18 +1,17 @@ package org.dnd.timeet.meeting.application; import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; - import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; - -import org.dnd.timeet.participant.domain.Participant; -import org.dnd.timeet.participant.domain.ParticipantRepository; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.member.domain.Member; -import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; +import org.dnd.timeet.participant.domain.Participant; +import org.dnd.timeet.participant.domain.ParticipantRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -78,4 +77,15 @@ public void removeParticipant(Long meetingId, Member member) { } + @Transactional(readOnly = true) + public List getMeetingMembers(Long meetingId) { + Meeting meeting = meetingRepository.findByIdWithParticipantsAndMembers(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + return meeting.getParticipants().stream() + .map(Participant::getMember) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index 32ecb2a..c252980 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -3,6 +3,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.security.CustomUserDetails; import org.dnd.timeet.common.utils.ApiUtils; @@ -12,6 +14,7 @@ import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; +import org.dnd.timeet.member.dto.MemberInfoResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; @@ -61,4 +64,16 @@ public ResponseEntity> getTimerById(@PathVariable return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); } + @GetMapping("/{meeting-id}/users") + @Operation(summary = "회의 참가자 조회", description = "회의에 참가한 사용자를 조회한다.") + public ResponseEntity getMeetingMembers(@PathVariable("meeting-id") Long meetingId) { + List memberInfoReponseList = meetingService.getMeetingMembers(meetingId) + .stream() + .map(MemberInfoResponse::from) + .collect(Collectors.toList()); + + return ResponseEntity.ok(ApiUtils.success(memberInfoReponseList)); + } + + } diff --git a/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java b/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java index f0f9304..920e1b6 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/MeetingRepository.java @@ -1,8 +1,13 @@ package org.dnd.timeet.meeting.domain; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MeetingRepository extends JpaRepository { -// Optional findByUserId(Long id); + @Query("select m from Meeting m join fetch m.participants p join fetch p.member where m.id = :meetingId") + Optional findByIdWithParticipantsAndMembers(@Param("meetingId") Long meetingId); + } diff --git a/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java b/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java index ef336c4..84a503e 100644 --- a/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java +++ b/src/main/java/org/dnd/timeet/member/dto/MemberInfoResponse.java @@ -4,7 +4,6 @@ import lombok.Getter; import lombok.Setter; import org.dnd.timeet.member.domain.Member; -import org.dnd.timeet.member.domain.MemberRole; @Getter @Setter @@ -13,22 +12,18 @@ public class MemberInfoResponse { @Schema(description = "사용자 id", nullable = false, example = "12") private long id; @Schema(description = "사용자 이름", nullable = false, example = "green12") - private String username; - @Schema(description = "사용자 역할", nullable = false, example = "ROLE_USER") - private MemberRole role; + private String name; - public MemberInfoResponse(long id, String username, MemberRole role) { + public MemberInfoResponse(long id, String name) { this.id = id; - this.username = username; - this.role = role; + this.name = name; } public static MemberInfoResponse from(Member member) { return new MemberInfoResponse( member.getId(), - member.getName(), - member.getRole() + member.getName() ); } } From 2f3b999b70ee43afa15ebe0a76723cafd89cf423 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Tue, 13 Feb 2024 01:29:56 +0900 Subject: [PATCH 46/81] [#26] feat: Add AgendaReport Controller and Service --- .../agenda/dto/AgendaReportInfoResponse.java | 44 +++++++++++++++++ .../dnd/timeet/common/utils/TimeUtils.java | 19 ++++++++ .../meeting/application/MeetingService.java | 48 +++++++++++++++++-- .../meeting/controller/MeetingController.java | 9 ++++ .../dto/MeetingReportInfoResponse.java | 34 +++++++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java create mode 100644 src/main/java/org/dnd/timeet/common/utils/TimeUtils.java create mode 100644 src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java new file mode 100644 index 0000000..1d2fb13 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java @@ -0,0 +1,44 @@ +package org.dnd.timeet.agenda.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Duration; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.common.utils.TimeUtils; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "회의 정보 응답 (리포트 생성용)") +@Getter +@Setter +public class AgendaReportInfoResponse { + + @Schema(description = "안건 id", example = "12") + private Long agendaId; + + @Schema(description = "안건 제목", example = "안건1") + private String title; + + @Schema(description = "소요 시간 차이 (실제 소요 시간 - 예상 소요 시간)", example = "01:30") + @DateTimeFormat(pattern = "HH:mm") + private Duration diff; + + + @Builder + public AgendaReportInfoResponse(Long agendaId, String title, + Duration diff) { + this.agendaId = agendaId; + this.title = title; + this.diff = diff; + } + + + public static AgendaReportInfoResponse from(Agenda agenda) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 + return AgendaReportInfoResponse.builder() + .agendaId(agenda.getId()) + .title(agenda.getTitle()) + .diff(TimeUtils.calculateTimeDiff(agenda.getEstimatedDuration(), agenda.getActualDuration())) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java b/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java new file mode 100644 index 0000000..40c4c3b --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java @@ -0,0 +1,19 @@ +package org.dnd.timeet.common.utils; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.Collections; +import org.dnd.timeet.common.exception.InternalServerError; + +public class TimeUtils { + + public static Duration calculateTimeDiff(LocalTime totalEstimatedDuration, LocalTime totalActualDuration) { + if (totalEstimatedDuration == null || totalActualDuration == null) { + throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("LocalTime", "LocalTime EstimatedDuration or ActualDuration is null")); + } + + return Duration.between(totalEstimatedDuration, totalActualDuration); + } + +} diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index ca76540..dcf03e2 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -1,18 +1,25 @@ package org.dnd.timeet.meeting.application; +import java.time.LocalTime; import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.agenda.domain.AgendaRepository; +import org.dnd.timeet.agenda.domain.AgendaStatus; +import org.dnd.timeet.agenda.domain.AgendaType; +import org.dnd.timeet.agenda.dto.AgendaReportInfoResponse; import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.common.exception.InternalServerError; import org.dnd.timeet.common.exception.NotFoundError; - +import org.dnd.timeet.common.utils.TimeUtils; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; - -import org.dnd.timeet.participant.domain.Participant; -import org.dnd.timeet.participant.domain.ParticipantRepository; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; +import org.dnd.timeet.meeting.dto.MeetingReportInfoResponse; import org.dnd.timeet.member.domain.Member; -import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; +import org.dnd.timeet.participant.domain.Participant; +import org.dnd.timeet.participant.domain.ParticipantRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +30,7 @@ public class MeetingService { private final MeetingRepository meetingRepository; private final ParticipantRepository participantRepository; + private final AgendaRepository agendaRepository; public Meeting createMeeting(MeetingCreateRequest createDto, Member member) { Meeting meeting = createDto.toEntity(member); @@ -60,6 +68,36 @@ public Meeting addParticipantToMeeting(Long meetingId, Member member) { return meeting; } + public MeetingReportInfoResponse createReport(Long meetingId) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + List agendaReportInfoResponses = agendaRepository.findByMeetingId(meetingId).stream() + .filter(agenda -> agenda.getType() == AgendaType.AGENDA && agenda.getStatus() == AgendaStatus.COMPLETED) + .map(AgendaReportInfoResponse::from) + .collect(Collectors.toList()); + + return MeetingReportInfoResponse.builder() + .totalDiff( + TimeUtils.calculateTimeDiff(meeting.getTotalEstimatedDuration(), meeting.getTotalActualDuration())) + .agendas(agendaReportInfoResponses) + .memos("회의록입니다.") + .build(); + + } + + private LocalTime calculateTotalDiff(LocalTime totalEstimatedDuration, LocalTime totalActualDuration) { + if (totalEstimatedDuration == null || totalActualDuration == null) { + throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("Meeting", "Meeting totalEstimatedDuration or totalActualDuration is null")); + } + + return totalEstimatedDuration.minusHours(totalActualDuration.getHour()) + .minusMinutes(totalActualDuration.getMinute()); + } + + @Transactional(readOnly = true) public Meeting findById(Long id) { return meetingRepository.findById(id) diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index 32ecb2a..73d8f99 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -12,6 +12,7 @@ import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; +import org.dnd.timeet.meeting.dto.MeetingReportInfoResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; @@ -61,4 +62,12 @@ public ResponseEntity> getTimerById(@PathVariable return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); } + @GetMapping("{meeting-id}/report") + @Operation(summary = "회의 리포트 조회", description = "회의 리포트를 조회한다.") + public ResponseEntity> getMeetingReport( + @PathVariable("meeting-id") Long meetingId) { + MeetingReportInfoResponse meetingReportInfoResponse = meetingService.createReport(meetingId); + + return ResponseEntity.ok(ApiUtils.success(meetingReportInfoResponse)); + } } diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java new file mode 100644 index 0000000..854147a --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java @@ -0,0 +1,34 @@ +package org.dnd.timeet.meeting.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Duration; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.dnd.timeet.agenda.dto.AgendaReportInfoResponse; +import org.springframework.format.annotation.DateTimeFormat; + +@Schema(description = "회의 리포트 응답") +@Getter +@Setter +public class MeetingReportInfoResponse { + + @Schema(description = "소요 시간 차이 (실제 소요 시간 - 예상 소요 시간)", example = "01:30") + @DateTimeFormat(pattern = "HH:mm") + private Duration totalDiff; + + @Schema(description = "안건 정보") + private List agendas; + + //TODO: 회의 메모 추가 예정 + @Schema(description = "회의 메모", example = "회의 내용 메모 (추가 예정)") + private String memos; + + @Builder + public MeetingReportInfoResponse(Duration totalDiff, List agendas, String memos) { + this.totalDiff = totalDiff; + this.agendas = agendas; + this.memos = memos; + } +} From b7a6d616d615670c0dc5684ed74e76230f311b7c Mon Sep 17 00:00:00 2001 From: FacerAin Date: Wed, 14 Feb 2024 13:52:27 +0900 Subject: [PATCH 47/81] [#27] feat: Add deleteMeeting --- .../meeting/application/MeetingService.java | 15 ++++++++++----- .../meeting/controller/MeetingController.java | 8 ++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index ca76540..34ef01d 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -4,15 +4,12 @@ import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; - import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; - -import org.dnd.timeet.participant.domain.Participant; -import org.dnd.timeet.participant.domain.ParticipantRepository; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.member.domain.Member; -import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; +import org.dnd.timeet.participant.domain.Participant; +import org.dnd.timeet.participant.domain.ParticipantRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -78,4 +75,12 @@ public void removeParticipant(Long meetingId, Member member) { } + public void cancelMeeting(Long meetingId) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + meeting.cancelMeeting(); + } + } diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index 32ecb2a..35c5424 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -14,6 +14,7 @@ import org.dnd.timeet.meeting.dto.MeetingInfoResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -61,4 +62,11 @@ public ResponseEntity> getTimerById(@PathVariable return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); } + @DeleteMapping("/{meeting-id}") + @Operation(summary = "회의 삭제", description = "지정된 id에 해당하는 회의를 삭제한다.") + public ResponseEntity deleteMeeting(@PathVariable("meeting-id") Long meetingId) { + meetingService.cancelMeeting(meetingId); + return ResponseEntity.noContent().build(); + } + } From e4d823d6ff20e7f07eb343fac05b6d4b4c0f2009 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Wed, 14 Feb 2024 14:27:37 +0900 Subject: [PATCH 48/81] [#28] feat: Add MemberInfoListResponse --- .../meeting/controller/MeetingController.java | 10 +++++++--- .../member/dto/MemberInfoListResponse.java | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/member/dto/MemberInfoListResponse.java diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index c252980..cfdd6ef 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -14,6 +14,7 @@ import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; +import org.dnd.timeet.member.dto.MemberInfoListResponse; import org.dnd.timeet.member.dto.MemberInfoResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -66,13 +67,16 @@ public ResponseEntity> getTimerById(@PathVariable @GetMapping("/{meeting-id}/users") @Operation(summary = "회의 참가자 조회", description = "회의에 참가한 사용자를 조회한다.") - public ResponseEntity getMeetingMembers(@PathVariable("meeting-id") Long meetingId) { - List memberInfoReponseList = meetingService.getMeetingMembers(meetingId) + public ResponseEntity> getMeetingMembers( + @PathVariable("meeting-id") Long meetingId) { + List memberInfoList = meetingService.getMeetingMembers(meetingId) .stream() .map(MemberInfoResponse::from) .collect(Collectors.toList()); - return ResponseEntity.ok(ApiUtils.success(memberInfoReponseList)); + MemberInfoListResponse memberInfoListResponse = new MemberInfoListResponse(memberInfoList); + + return ResponseEntity.ok(ApiUtils.success(memberInfoListResponse)); } diff --git a/src/main/java/org/dnd/timeet/member/dto/MemberInfoListResponse.java b/src/main/java/org/dnd/timeet/member/dto/MemberInfoListResponse.java new file mode 100644 index 0000000..83ce930 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/dto/MemberInfoListResponse.java @@ -0,0 +1,18 @@ +package org.dnd.timeet.member.dto; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MemberInfoListResponse { + + + private List members; + + + public MemberInfoListResponse(List members) { + this.members = members; + } +} From 09b201ce24039c0a0874a793aa994b2ed092bc16 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Wed, 14 Feb 2024 14:51:52 +0900 Subject: [PATCH 49/81] [#32] feat: Add deleteAgenda --- .../agenda/application/AgendaService.java | 10 +++++++- .../agenda/controller/AgendaController.java | 25 +++++++++++++------ .../org/dnd/timeet/agenda/domain/Agenda.java | 5 ++++ .../timeet/agenda/domain/AgendaStatus.java | 2 +- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index 9f1b28e..71d3776 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -15,7 +15,6 @@ import org.dnd.timeet.meeting.domain.MeetingRepository; import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.participant.domain.ParticipantRepository; -import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -72,4 +71,13 @@ public Agenda changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequ return agendaRepository.save(agenda); // 변경된 안건 상태로 응답 객체 생성 및 반환 } + + public void cancelAgenda(Long meetingId, Long agendaId) { + Agenda agenda = agendaRepository.findByIdAndMeetingId(agendaId, meetingId) + .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("AgendaId", "Agenda not found"))); + agenda.delete(); + } + + } diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index dffb6b6..fc0a59a 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -20,6 +20,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -59,13 +60,13 @@ public ResponseEntity getAgendas( } /* - * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. - * @SendTo : 브로커에게 메세지를 보낸다. - * 과정 - * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. - * 2. 핸들러가 메세지를 처리한다. - * 3. 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. - * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. + * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. + * @SendTo : 브로커에게 메세지를 보낸다. + * 과정 + * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. + * 2. 핸들러가 메세지를 처리한다. + * 3. 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. + * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. */ @Operation(summary = "안건 제어 및 갱신", description = "해당 안건을 제어 및 갱신한다.") @MessageMapping("/meeting/{meeting-id}/agendas/{agenda-id}/action") @@ -78,4 +79,14 @@ public AgendaActionResponse handleAgendaAction(@DestinationVariable Long meeting // 변경된 안건 상태로 응답 객체 생성 및 반환 return new AgendaActionResponse(agenda.getId(), agenda.getStatus()); } + + @DeleteMapping("/{meeting-id}/agendas/{agenda-id}") + @Operation(summary = "안건 삭제", description = "지정된 ID에 해당하는 안건을 삭제한다.") + public ResponseEntity deleteAgenda( + @PathVariable("meeting-id") Long meetingId, + @PathVariable("agenda-id") Long agendaId) { + agendaService.cancelAgenda(meetingId, agendaId); + + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 2663530..983cb73 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -120,5 +120,10 @@ private LocalTime calculateActualDuration() { return this.estimatedDuration; } + public void cancelAgenda() { + this.status = AgendaStatus.CANCELED; + this.delete(); + } + } diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java index 00a0949..0b21747 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java @@ -1,5 +1,5 @@ package org.dnd.timeet.agenda.domain; public enum AgendaStatus { - PENDING, INPROGRESS, PAUSED, COMPLETED + PENDING, INPROGRESS, PAUSED, COMPLETED, CANCELED } \ No newline at end of file From 8c960cef88b82e3f2dfcd6d097cb0575e74cad83 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 14 Feb 2024 17:06:41 +0900 Subject: [PATCH 50/81] [#24] refactor: Overhaul timer logic to support pause, extension, and late joiner tracking --- .../org/dnd/timeet/agenda/domain/Agenda.java | 122 ++++++++++++------ 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 2663530..82425d4 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -10,7 +10,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import java.time.LocalTime; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.Collections; import lombok.AccessLevel; import lombok.Builder; @@ -39,17 +40,25 @@ public class Agenda extends AuditableEntity { @Enumerated(EnumType.STRING) private AgendaType type; - // 예상 소요 시간 - @Column(nullable = false, name = "estimated_duration") - private LocalTime estimatedDuration; + // 할당된 총 시간 + @Column(name = "allocated_duration", nullable = false) + private Duration allocatedDuration = Duration.ZERO; - // 연장된 총 시간 - @Column(nullable = true, name = "extended_duration") - private LocalTime extendedDuration; + // 안건 시작 시간 + @Column(name = "start_time") + private LocalDateTime startTime; - // 실제 소요 시간 - @Column(nullable = true, name = "actual_duration") - private LocalTime actualDuration; + // 안건 일시정지 시간 + @Column(name = "pause_time") + private LocalDateTime pauseTime; + + // 일시정지된 시간 누적 + @Column(name = "paused_duration") + private Duration pausedDuration = Duration.ZERO; + + // 안건 총 소요시간 + @Column(name = "total_duration") + private Duration totalDuration; @Column(nullable = false, name = "order_num") private Integer orderNum; @@ -57,67 +66,94 @@ public class Agenda extends AuditableEntity { @Enumerated(EnumType.STRING) private AgendaStatus status = AgendaStatus.PENDING; - @Builder - public Agenda(Long id, Meeting meeting, String title, AgendaType type, LocalTime estimatedDuration, - Integer orderNum, AgendaStatus status) { - this.id = id; + public Agenda(Meeting meeting, String title, AgendaType type, Duration allocatedDuration, Integer orderNum) { this.meeting = meeting; this.title = title; this.type = type; - this.estimatedDuration = estimatedDuration; + this.allocatedDuration = allocatedDuration; this.orderNum = orderNum; - this.status = status; } + public void start() { - if (this.status != AgendaStatus.PENDING) { - throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, - Collections.singletonMap("AgendaStatus", "Agenda can only be started from PENDING status")); - } + validateTransition(AgendaStatus.PENDING); + this.startTime = LocalDateTime.now(); this.status = AgendaStatus.INPROGRESS; } public void pause() { - if (this.status != AgendaStatus.INPROGRESS) { - throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, - Collections.singletonMap("AgendaStatus", "Agenda can only be paused from INPROGRESS status.")); - } + validateTransition(AgendaStatus.INPROGRESS); + this.pauseTime = LocalDateTime.now(); // 일시정지 시간 설정 this.status = AgendaStatus.PAUSED; } public void resume() { - if (this.status != AgendaStatus.PAUSED) { - throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, - Collections.singletonMap("AgendaStatus", "Agenda can only be resumed from PAUSED status.")); - } + validateTransition(AgendaStatus.PAUSED); + // 일시정지된 시간 누적 + this.pausedDuration = this.pausedDuration.plus(Duration.between(pauseTime, LocalDateTime.now())); + this.pauseTime = null; this.status = AgendaStatus.INPROGRESS; } - public void extendDuration(LocalTime extension) { - if (this.extendedDuration == null) { - this.extendedDuration = extension; - } else { - this.extendedDuration = this.extendedDuration.plusHours(extension.getHour()) - .plusMinutes(extension.getMinute()); + public void extendDuration(Duration extension) { + if (this.status != AgendaStatus.COMPLETED) { + this.allocatedDuration = this.allocatedDuration.plus(extension); } } public void complete() { - if (this.status == AgendaStatus.COMPLETED) { + validateTransition(AgendaStatus.INPROGRESS, AgendaStatus.PAUSED); + + this.totalDuration = calculateCurrentDuration(); + this.status = AgendaStatus.COMPLETED; + } + + // 현재까지 진행된 시간 계산 + public Duration calculateCurrentDuration() { + LocalDateTime now = LocalDateTime.now(); + + if (this.status == AgendaStatus.PENDING) { + return Duration.ZERO; + } else if (this.status == AgendaStatus.COMPLETED) { + return this.totalDuration; + } else { // INPROGRESS 또는 PAUSED 상태 + Duration passedTime; + // PAUSED 상태라면, 마지막 일시정지 시간부터 현재까지의 시간을 뺀다 + if (this.status == AgendaStatus.PAUSED && this.pauseTime != null) { + passedTime = Duration.between(this.startTime, this.pauseTime).minus(this.pausedDuration); + } else { // INPROGRESS 상태라면, 시작 시간부터 현재까지의 시간을 뺀다 + passedTime = Duration.between(this.startTime, now).minus(this.pausedDuration); + } + return passedTime; + } + } + + // 남은 시간 계산 메서드 수정 + public Duration calculateRemainingTime() { + ensureInProgressOrPaused(); + // 현재까지 진행된 시간 계산 + Duration passedTime = calculateCurrentDuration(); + // 할당된 총 시간에서 현재까지 진행된 시간을 뺀다 + return this.allocatedDuration.minus(passedTime); + } + + private void ensureInProgressOrPaused() { + if (!(this.status == AgendaStatus.INPROGRESS || this.status == AgendaStatus.PAUSED)) { throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, - Collections.singletonMap("AgendaStatus", "Agenda is already completed.")); + Collections.singletonMap("AgendaStatus", + "Operation is only allowed for agendas in INPROGRESS or PAUSED status.")); } - this.status = AgendaStatus.COMPLETED; - this.actualDuration = calculateActualDuration(); // 실제 소요 시간 계산 } - private LocalTime calculateActualDuration() { - if (this.extendedDuration != null) { - return this.estimatedDuration.plusHours(this.extendedDuration.getHour()) - .plusMinutes(this.extendedDuration.getMinute()); + private void validateTransition(AgendaStatus... validPreviousStatuses) { + for (AgendaStatus validStatus : validPreviousStatuses) { + if (this.status == validStatus) { + return; + } } - return this.estimatedDuration; + throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("AgendaStatus", "Invalid status transition")); } } From 93b61e3f73a2853c3610dcaf3818b41b9946e7db Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 14 Feb 2024 19:49:28 +0900 Subject: [PATCH 51/81] [#24] refactor: Update calculateRemainingTime for clarity and accuracy Revise the calculateRemainingTime method in Agenda entity to enhance readability and ensure accurate remaining time calculation across all agenda statuses. --- .../org/dnd/timeet/agenda/domain/Agenda.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 82425d4..3ad10a6 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -129,20 +129,17 @@ public Duration calculateCurrentDuration() { } } - // 남은 시간 계산 메서드 수정 + // 남은 시간 계산 메서드 public Duration calculateRemainingTime() { - ensureInProgressOrPaused(); - // 현재까지 진행된 시간 계산 - Duration passedTime = calculateCurrentDuration(); - // 할당된 총 시간에서 현재까지 진행된 시간을 뺀다 - return this.allocatedDuration.minus(passedTime); - } - - private void ensureInProgressOrPaused() { - if (!(this.status == AgendaStatus.INPROGRESS || this.status == AgendaStatus.PAUSED)) { - throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION, - Collections.singletonMap("AgendaStatus", - "Operation is only allowed for agendas in INPROGRESS or PAUSED status.")); + if (this.status == AgendaStatus.PENDING) { + return this.allocatedDuration; + } else if (this.status == AgendaStatus.COMPLETED) { + return Duration.ZERO; + } else { // INPROGRESS 또는 PAUSED 상태 + // 현재까지 진행된 시간 계산 + Duration passedTime = calculateCurrentDuration(); + // 할당된 총 시간에서 현재까지 진행된 시간을 뺀다 + return this.allocatedDuration.minus(passedTime); } } From 57d37360be1a409373ed4e384b3a7c24d46a4ae5 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 14 Feb 2024 19:55:08 +0900 Subject: [PATCH 52/81] [#24] feat: Add DateTimeUtils and DurationUtils for formatting LocalDateTime and handling Duration operations --- .../timeet/common/utils/DateTimeUtils.java | 23 +++++++++++ .../timeet/common/utils/DurationUtils.java | 41 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/common/utils/DateTimeUtils.java create mode 100644 src/main/java/org/dnd/timeet/common/utils/DurationUtils.java diff --git a/src/main/java/org/dnd/timeet/common/utils/DateTimeUtils.java b/src/main/java/org/dnd/timeet/common/utils/DateTimeUtils.java new file mode 100644 index 0000000..066ae48 --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/utils/DateTimeUtils.java @@ -0,0 +1,23 @@ +package org.dnd.timeet.common.utils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtils { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + /** + * LocalDateTime 객체를 yyyy-MM-dd'T'HH:mm:ss 형식의 문자열로 포매팅한다. + * + * @param localDateTime LocalDateTime 객체 + * @return 포매팅된 문자열 + */ + public static String formatLocalDateTime(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return localDateTime.format(FORMATTER); + } +} + diff --git a/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java b/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java new file mode 100644 index 0000000..0dadeb3 --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java @@ -0,0 +1,41 @@ +package org.dnd.timeet.common.utils; + +import java.time.Duration; +import java.time.LocalTime; + +public class DurationUtils { + + /** + * LocalTime 객체를 Duration으로 변환한다. + * @param time LocalTime 객체 + * @return Duration 객체 + */ + public static Duration convertLocalTimeToDuration(LocalTime time) { + int totalMinutes = time.getHour() * 60 + time.getMinute(); + return Duration.ofMinutes(totalMinutes); + } + + /** + * Duration 객체를 HH:mm 형식의 문자열로 포매팅한다. + * @param duration Duration 객체 + * @return 포매팅된 시간 문자열 (HH:mm) + */ + public static String formatDuration(Duration duration) { + long hours = duration.toHours(); + long minutes = duration.toMinutes() % 60; + return String.format("%02d:%02d", hours, minutes); + } + + public static void main(String[] args) { + // 변환 테스트 + String timeString = "02:30"; + LocalTime time = LocalTime.parse(timeString); + Duration duration = convertLocalTimeToDuration(time); + System.out.println("Converted Duration: " + duration); // PT2H30M + + // 포매팅 테스트 + String formattedDuration = formatDuration(duration); + System.out.println("Formatted Duration: " + formattedDuration); // 02:30 + } +} + From d9ab7f87aafa75da3b58493f7b78146ee605db5c Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 14 Feb 2024 19:55:50 +0900 Subject: [PATCH 53/81] [#24] feat: Align agenda controller and service with updated logic --- .../agenda/application/AgendaService.java | 16 +++++-- .../agenda/controller/AgendaController.java | 20 ++++---- .../agenda/dto/AgendaActionResponse.java | 30 ++++++++++-- .../agenda/dto/AgendaCreateRequest.java | 8 ++-- .../timeet/agenda/dto/AgendaInfoResponse.java | 48 ++++++------------- 5 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index b7592db..548990d 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -1,5 +1,6 @@ package org.dnd.timeet.agenda.application; +import java.time.Duration; import java.time.LocalTime; import java.util.Collections; import java.util.List; @@ -7,15 +8,16 @@ import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.domain.AgendaRepository; import org.dnd.timeet.agenda.dto.AgendaActionRequest; +import org.dnd.timeet.agenda.dto.AgendaActionResponse; import org.dnd.timeet.agenda.dto.AgendaCreateRequest; import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; import org.dnd.timeet.common.exception.NotFoundError.ErrorCode; +import org.dnd.timeet.common.utils.DurationUtils; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.participant.domain.ParticipantRepository; -import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +34,8 @@ public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto, Member Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("MeetingId", "Meeting not found"))); + + // 회의에 참가한 멤버인지 확인 participantRepository.findByMeetingIdAndMemberId(meetingId, member.getId()) .orElseThrow(() -> new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED, Collections.singletonMap("MemberId", "Member is not a participant of the meeting"))); @@ -46,7 +50,7 @@ public List findAll(Long meetingId) { return agendaRepository.findByMeetingId(meetingId); } - public Agenda changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequest actionRequest) { + public AgendaActionResponse changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequest actionRequest) { Agenda agenda = agendaRepository.findByIdAndMeetingId(agendaId, meetingId) .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("AgendaId", "Agenda not found"))); @@ -66,13 +70,17 @@ public Agenda changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequ break; case "modify": LocalTime modifiedDuration = LocalTime.parse(actionRequest.getModifiedDuration()); - agenda.extendDuration(modifiedDuration); + agenda.extendDuration(DurationUtils.convertLocalTimeToDuration(modifiedDuration)); break; default: throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED, Collections.singletonMap("Action", "Invalid action")); } + Agenda savedAgenda = agendaRepository.save(agenda); + + Duration currentDuration = savedAgenda.calculateCurrentDuration(); + Duration remainingDuration = agenda.calculateRemainingTime(); - return agendaRepository.save(agenda); // 변경된 안건 상태로 응답 객체 생성 및 반환 + return new AgendaActionResponse(savedAgenda, currentDuration, remainingDuration); } } diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index d770305..46a96ce 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -52,20 +52,20 @@ public ResponseEntity getAgendas( @PathVariable("meeting-id") Long meetingId) { List agendaInfoResponseList = agendaService.findAll(meetingId) .stream() - .map(AgendaInfoResponse::from) + .map(a -> new AgendaInfoResponse(a, a.calculateCurrentDuration(), a.calculateRemainingTime())) .collect(Collectors.toList()); return ResponseEntity.ok(ApiUtils.success(agendaInfoResponseList)); } /* - * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. - * @SendTo : 브로커에게 메세지를 보낸다. - * 과정 - * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. - * 2. 핸들러가 메세지를 처리한다. - * 3. 서버는 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. - * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. + * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. + * @SendTo : 브로커에게 메세지를 보낸다. + * 과정 + * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. + * 2. 핸들러가 메세지를 처리한다. + * 3. 서버는 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. + * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. */ @Operation(summary = "안건 제어 및 갱신", description = "해당 안건을 제어 및 갱신한다.") @MessageMapping("/meeting/{meeting-id}/agendas/{agenda-id}/action") @@ -74,8 +74,6 @@ public AgendaActionResponse handleAgendaAction(@DestinationVariable("meeting-id" @DestinationVariable("agenda-id") Long agendaId, AgendaActionRequest actionRequest) { // 로직 구현 (안건 상태 변경) - Agenda agenda = agendaService.changeAgendaStatus(meetingId, agendaId, actionRequest); - // 변경된 안건 상태로 응답 객체 생성 및 반환 - return new AgendaActionResponse(agenda.getId(), agenda.getStatus()); + return agendaService.changeAgendaStatus(meetingId, agendaId, actionRequest); } } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java index 3e43b8e..94f195e 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaActionResponse.java @@ -1,21 +1,43 @@ package org.dnd.timeet.agenda.dto; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Duration; +import java.time.LocalDateTime; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import org.dnd.timeet.agenda.domain.Agenda; import org.dnd.timeet.agenda.domain.AgendaStatus; +import org.dnd.timeet.agenda.domain.AgendaType; +import org.dnd.timeet.common.utils.DateTimeUtils; +import org.dnd.timeet.common.utils.DurationUtils; @Schema(description = "안건 제어 응답") @Getter @Setter public class AgendaActionResponse { + private Long agendaId; private AgendaStatus status; + private String title; + private AgendaType type; + + private String currentDuration; // 현재까지 진행된 시간 + private String remainingDuration; // 남은 시간 + + private Integer orderNum; + private String timestamp; // 수정 시간 + + public AgendaActionResponse(Agenda agenda, Duration currentDuration, Duration remainingDuration) { + this.agendaId = agenda.getId(); + this.status = agenda.getStatus(); + this.title = agenda.getTitle(); + this.type = agenda.getType(); + + this.currentDuration = DurationUtils.formatDuration(currentDuration); + this.remainingDuration = DurationUtils.formatDuration(remainingDuration); - public AgendaActionResponse(Long agendaId, AgendaStatus status) { - this.agendaId = agendaId; - this.status = status; + this.orderNum = agenda.getOrderNum(); + this.timestamp = DateTimeUtils.formatLocalDateTime(LocalDateTime.now()); } } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java index c23b4fc..2462ce5 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaCreateRequest.java @@ -2,14 +2,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.time.LocalDateTime; import java.time.LocalTime; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.dnd.timeet.agenda.domain.Agenda; -import org.dnd.timeet.agenda.domain.AgendaStatus; import org.dnd.timeet.agenda.domain.AgendaType; +import org.dnd.timeet.common.utils.DurationUtils; import org.dnd.timeet.meeting.domain.Meeting; import org.springframework.format.annotation.DateTimeFormat; @@ -31,7 +30,7 @@ public class AgendaCreateRequest { @NotNull(message = "안건 소요 시간은 반드시 입력되어야 합니다") @DateTimeFormat(pattern = "HH:mm") @Schema(description = "안건 소요 시간", example = "01:20") - private LocalTime estimatedDuration; + private LocalTime allocatedDuration; @NotNull(message = "안건 순서는 반드시 입력되어야 합니다") @Schema(description = "안건 순서", example = "1") @@ -42,9 +41,8 @@ public Agenda toEntity(Meeting meeting) { .meeting(meeting) .title(this.title) .type(this.type.equals("AGENDA") ? AgendaType.AGENDA : AgendaType.BREAK) - .estimatedDuration(this.estimatedDuration) + .allocatedDuration(DurationUtils.convertLocalTimeToDuration(this.allocatedDuration)) .orderNum(this.orderNum) - .status(AgendaStatus.PENDING) .build(); } } diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java index 3aca973..9170978 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java @@ -1,13 +1,13 @@ package org.dnd.timeet.agenda.dto; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalTime; -import lombok.Builder; +import java.time.Duration; import lombok.Getter; import lombok.Setter; import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.common.utils.DurationUtils; -@Schema(description = "회의 정보 응답") +@Schema(description = "안건 정보 응답") @Getter @Setter public class AgendaInfoResponse { @@ -15,45 +15,27 @@ public class AgendaInfoResponse { @Schema(description = "안건 id", example = "12") private Long agendaId; - @Schema(description = "안건 제목", example = "안건1") + @Schema(description = "안건 제목", example = "안건 1") private String title; @Schema(description = "안건 종류", example = "AGENDA") private String type; - @Schema(description = "예상 소요시간", example = "01:00") - private LocalTime estimatedDuration; + @Schema(description = "현재까지 소요된 시간", example = "00:36") + private String currentDuration; - @Schema(description = "연장된 총 시간", example = "00:30") - private LocalTime extendedDuration; - - @Schema(description = "실제 소요시간", example = "01:30") - private LocalTime actualDuration; + @Schema(description = "남은 시간", example = "00:24") + private String remainingDuration; @Schema(description = "안건 상태", example = "INPROGRESS") private String status; - @Builder - public AgendaInfoResponse(Long agendaId, String title, String type, LocalTime estimatedDuration, LocalTime extendedDuration, LocalTime actualDuration, String status) { - this.agendaId = agendaId; - this.title = title; - this.type = type; - this.estimatedDuration = estimatedDuration; - this.extendedDuration = extendedDuration; - this.actualDuration = actualDuration; - this.status = status; - } - - - public static AgendaInfoResponse from(Agenda agenda) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 - return AgendaInfoResponse.builder() - .agendaId(agenda.getId()) - .title(agenda.getTitle()) - .type(agenda.getType().name()) - .estimatedDuration(agenda.getEstimatedDuration()) - .extendedDuration(agenda.getExtendedDuration()) - .actualDuration(agenda.getActualDuration()) - .status(agenda.getStatus().name()) - .build(); + public AgendaInfoResponse(Agenda agenda, Duration currentDuration, Duration remainingDuration) { + this.agendaId = agenda.getId(); + this.title = agenda.getTitle(); + this.type = agenda.getType().name(); + this.currentDuration = DurationUtils.formatDuration(currentDuration); + this.remainingDuration = DurationUtils.formatDuration(remainingDuration); + this.status = agenda.getStatus().name(); } } \ No newline at end of file From b4ff398cc6ec290e169ab433e1b8d7c2f5f38130 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Wed, 14 Feb 2024 22:34:50 +0900 Subject: [PATCH 54/81] [#24] refactor: Add CANCELED status and update logic accordingly - Add CANCELED status to AgendaStatus enum. - Adjust calculateCurrentDuration to consider CANCELED status. - Modify calculateRemainingTime to return Duration.ZERO for CANCELED agendas. - Ensure all related unit tests are updated to cover the new logic. --- .../org/dnd/timeet/agenda/controller/AgendaController.java | 4 ++-- src/main/java/org/dnd/timeet/agenda/domain/Agenda.java | 4 ++-- src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java index 46a96ce..95a7d2b 100644 --- a/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java +++ b/src/main/java/org/dnd/timeet/agenda/controller/AgendaController.java @@ -62,9 +62,9 @@ public ResponseEntity getAgendas( * @MessageMapping : 클라이언트에서 해당 url로 메세지를 보내면 요청을 처리한다. * @SendTo : 브로커에게 메세지를 보낸다. * 과정 - * 1. 클라이언트에서 /app/meeting/{meetingId}/agendas/{agendaId}/action 으로 메세지를 보낸다. + * 1. 클라이언트에서 /app/meeting/{meeting-id}/agendas/{agenda-id}/action 으로 메세지를 보낸다. * 2. 핸들러가 메세지를 처리한다. - * 3. 서버는 그 결과를 /topic/meeting/{meetingId}/agendas/{agendaId}/status 주소로 브로커에게 보낸다. + * 3. 서버는 그 결과를 /topic/meeting/{meeting-id}/agendas/{agenda-id}/status 주소로 브로커에게 보낸다. * 4. 브로커는 해당 주소를 구독하고 있는 클라이언트에게 메세지를 전달한다. */ @Operation(summary = "안건 제어 및 갱신", description = "해당 안건을 제어 및 갱신한다.") diff --git a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java index 3ad10a6..d7a69c9 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/Agenda.java @@ -113,7 +113,7 @@ public void complete() { public Duration calculateCurrentDuration() { LocalDateTime now = LocalDateTime.now(); - if (this.status == AgendaStatus.PENDING) { + if (this.status == AgendaStatus.PENDING || this.status == AgendaStatus.CANCELED) { return Duration.ZERO; } else if (this.status == AgendaStatus.COMPLETED) { return this.totalDuration; @@ -133,7 +133,7 @@ public Duration calculateCurrentDuration() { public Duration calculateRemainingTime() { if (this.status == AgendaStatus.PENDING) { return this.allocatedDuration; - } else if (this.status == AgendaStatus.COMPLETED) { + } else if (this.status == AgendaStatus.COMPLETED || this.status == AgendaStatus.CANCELED) { return Duration.ZERO; } else { // INPROGRESS 또는 PAUSED 상태 // 현재까지 진행된 시간 계산 diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java index 00a0949..0b21747 100644 --- a/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaStatus.java @@ -1,5 +1,5 @@ package org.dnd.timeet.agenda.domain; public enum AgendaStatus { - PENDING, INPROGRESS, PAUSED, COMPLETED + PENDING, INPROGRESS, PAUSED, COMPLETED, CANCELED } \ No newline at end of file From f3c26282e07a8902d0f70fcdadcf3d9b609cfd6e Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 15 Feb 2024 11:04:05 +0900 Subject: [PATCH 55/81] [#26] feat: Update report duration logic --- .../agenda/dto/AgendaReportInfoResponse.java | 15 ++++++------ .../dnd/timeet/common/utils/TimeUtils.java | 23 +++++++++++++++---- .../meeting/application/MeetingService.java | 20 ++++------------ .../dto/MeetingReportInfoResponse.java | 13 ++++------- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java index 1d2fb13..d311ad1 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java @@ -1,13 +1,13 @@ package org.dnd.timeet.agenda.dto; +import static org.dnd.timeet.common.utils.TimeUtils.calculateTimeDiff; +import static org.dnd.timeet.common.utils.TimeUtils.formatDuration; + import io.swagger.v3.oas.annotations.media.Schema; -import java.time.Duration; import lombok.Builder; import lombok.Getter; import lombok.Setter; import org.dnd.timeet.agenda.domain.Agenda; -import org.dnd.timeet.common.utils.TimeUtils; -import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "회의 정보 응답 (리포트 생성용)") @Getter @@ -20,14 +20,13 @@ public class AgendaReportInfoResponse { @Schema(description = "안건 제목", example = "안건1") private String title; - @Schema(description = "소요 시간 차이 (실제 소요 시간 - 예상 소요 시간)", example = "01:30") - @DateTimeFormat(pattern = "HH:mm") - private Duration diff; + @Schema(description = "소요 시간 차이 (실제 소요 시간 - 예상 소요 시간)", example = "+01:30") + private String diff; @Builder public AgendaReportInfoResponse(Long agendaId, String title, - Duration diff) { + String diff) { this.agendaId = agendaId; this.title = title; this.diff = diff; @@ -38,7 +37,7 @@ public static AgendaReportInfoResponse from(Agenda agenda) { // 매개변수로 return AgendaReportInfoResponse.builder() .agendaId(agenda.getId()) .title(agenda.getTitle()) - .diff(TimeUtils.calculateTimeDiff(agenda.getEstimatedDuration(), agenda.getActualDuration())) + .diff(formatDuration(calculateTimeDiff(agenda.getActualDuration(), agenda.getEstimatedDuration()))) .build(); } } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java b/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java index 40c4c3b..c61f364 100644 --- a/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java +++ b/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java @@ -7,13 +7,28 @@ public class TimeUtils { - public static Duration calculateTimeDiff(LocalTime totalEstimatedDuration, LocalTime totalActualDuration) { - if (totalEstimatedDuration == null || totalActualDuration == null) { + public static Duration calculateTimeDiff(LocalTime ActualDuration, LocalTime EstimatedDuration) { + + if (ActualDuration == null) { + throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("LocalTime", "ActualDuration is null")); + } + if (EstimatedDuration == null) { throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, - Collections.singletonMap("LocalTime", "LocalTime EstimatedDuration or ActualDuration is null")); + Collections.singletonMap("LocalTime", "EstimatedDuration is null")); } - return Duration.between(totalEstimatedDuration, totalActualDuration); + return Duration.between(EstimatedDuration, ActualDuration); + } + + public static String formatDuration(Duration duration) { + long hours = duration.abs().toHours(); + long minutes = duration.minusHours(duration.toHours()).abs().toMinutes(); + String prefix = "+"; + if (duration.isNegative()) { + prefix = "-"; + } + return prefix + String.format("%02d:%02d", hours, minutes); } } diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index dcf03e2..f35aab5 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -1,6 +1,8 @@ package org.dnd.timeet.meeting.application; -import java.time.LocalTime; +import static org.dnd.timeet.common.utils.TimeUtils.calculateTimeDiff; +import static org.dnd.timeet.common.utils.TimeUtils.formatDuration; + import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -10,9 +12,7 @@ import org.dnd.timeet.agenda.domain.AgendaType; import org.dnd.timeet.agenda.dto.AgendaReportInfoResponse; import org.dnd.timeet.common.exception.BadRequestError; -import org.dnd.timeet.common.exception.InternalServerError; import org.dnd.timeet.common.exception.NotFoundError; -import org.dnd.timeet.common.utils.TimeUtils; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; @@ -80,24 +80,14 @@ public MeetingReportInfoResponse createReport(Long meetingId) { return MeetingReportInfoResponse.builder() .totalDiff( - TimeUtils.calculateTimeDiff(meeting.getTotalEstimatedDuration(), meeting.getTotalActualDuration())) + formatDuration( + calculateTimeDiff(meeting.getTotalActualDuration(), meeting.getTotalEstimatedDuration()))) .agendas(agendaReportInfoResponses) .memos("회의록입니다.") .build(); } - private LocalTime calculateTotalDiff(LocalTime totalEstimatedDuration, LocalTime totalActualDuration) { - if (totalEstimatedDuration == null || totalActualDuration == null) { - throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, - Collections.singletonMap("Meeting", "Meeting totalEstimatedDuration or totalActualDuration is null")); - } - - return totalEstimatedDuration.minusHours(totalActualDuration.getHour()) - .minusMinutes(totalActualDuration.getMinute()); - } - - @Transactional(readOnly = true) public Meeting findById(Long id) { return meetingRepository.findById(id) diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java index 854147a..8310be8 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportInfoResponse.java @@ -1,32 +1,29 @@ package org.dnd.timeet.meeting.dto; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.Duration; import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.Setter; import org.dnd.timeet.agenda.dto.AgendaReportInfoResponse; -import org.springframework.format.annotation.DateTimeFormat; @Schema(description = "회의 리포트 응답") @Getter @Setter public class MeetingReportInfoResponse { - @Schema(description = "소요 시간 차이 (실제 소요 시간 - 예상 소요 시간)", example = "01:30") - @DateTimeFormat(pattern = "HH:mm") - private Duration totalDiff; + @Schema(description = "소요 시간 차이 (실제 소요 시간 - 예상 소요 시간)", example = "+01:30") + private String totalDiff; @Schema(description = "안건 정보") private List agendas; - //TODO: 회의 메모 추가 예정 - @Schema(description = "회의 메모", example = "회의 내용 메모 (추가 예정)") + //TODO: 회의 메모 추가 + @Schema(description = "회의 메모", example = "회의 내용 메모") private String memos; @Builder - public MeetingReportInfoResponse(Duration totalDiff, List agendas, String memos) { + public MeetingReportInfoResponse(String totalDiff, List agendas, String memos) { this.totalDiff = totalDiff; this.agendas = agendas; this.memos = memos; From a8e84f0434502749227980d0e51601770c02bb14 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 15 Feb 2024 12:11:56 +0900 Subject: [PATCH 56/81] [#34] feat: Implement enum for AgendaAction (START, PAUSE, etc) --- .../agenda/application/AgendaService.java | 23 ++++++++++++++----- .../timeet/agenda/domain/AgendaAction.java | 10 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/agenda/domain/AgendaAction.java diff --git a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java index 548990d..f6e3989 100644 --- a/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java +++ b/src/main/java/org/dnd/timeet/agenda/application/AgendaService.java @@ -6,6 +6,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.dnd.timeet.agenda.domain.Agenda; +import org.dnd.timeet.agenda.domain.AgendaAction; import org.dnd.timeet.agenda.domain.AgendaRepository; import org.dnd.timeet.agenda.dto.AgendaActionRequest; import org.dnd.timeet.agenda.dto.AgendaActionResponse; @@ -55,20 +56,30 @@ public AgendaActionResponse changeAgendaStatus(Long meetingId, Long agendaId, Ag .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("AgendaId", "Agenda not found"))); - switch (actionRequest.getAction()) { - case "start": + String actionString = actionRequest.getAction().toUpperCase(); + AgendaAction action; + + try { + action = AgendaAction.valueOf(actionString); + } catch (IllegalArgumentException e) { + throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED, + Collections.singletonMap("Action", "Invalid action")); + } + + switch (action) { + case START: agenda.start(); break; - case "pause": + case PAUSE: agenda.pause(); break; - case "resume": + case RESUME: agenda.resume(); break; - case "end": + case END: agenda.complete(); break; - case "modify": + case MODIFY: LocalTime modifiedDuration = LocalTime.parse(actionRequest.getModifiedDuration()); agenda.extendDuration(DurationUtils.convertLocalTimeToDuration(modifiedDuration)); break; diff --git a/src/main/java/org/dnd/timeet/agenda/domain/AgendaAction.java b/src/main/java/org/dnd/timeet/agenda/domain/AgendaAction.java new file mode 100644 index 0000000..595d2c3 --- /dev/null +++ b/src/main/java/org/dnd/timeet/agenda/domain/AgendaAction.java @@ -0,0 +1,10 @@ +package org.dnd.timeet.agenda.domain; + +public enum AgendaAction { + START, PAUSE, RESUME, END, MODIFY; + + @Override + public String toString() { + return name().toLowerCase(); + } +} From efdff961f04c3076cbd4260869f36d23a8c59231 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 15 Feb 2024 12:20:21 +0900 Subject: [PATCH 57/81] [#34] refactor: Extract DurationUtils test for main class --- .../timeet/common/utils/DurationUtils.java | 14 ++----------- .../common/utils/DurationUtilsTest.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/dnd/timeet/common/utils/DurationUtilsTest.java diff --git a/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java b/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java index 0dadeb3..998fadb 100644 --- a/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java +++ b/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java @@ -7,6 +7,7 @@ public class DurationUtils { /** * LocalTime 객체를 Duration으로 변환한다. + * * @param time LocalTime 객체 * @return Duration 객체 */ @@ -17,6 +18,7 @@ public static Duration convertLocalTimeToDuration(LocalTime time) { /** * Duration 객체를 HH:mm 형식의 문자열로 포매팅한다. + * * @param duration Duration 객체 * @return 포매팅된 시간 문자열 (HH:mm) */ @@ -25,17 +27,5 @@ public static String formatDuration(Duration duration) { long minutes = duration.toMinutes() % 60; return String.format("%02d:%02d", hours, minutes); } - - public static void main(String[] args) { - // 변환 테스트 - String timeString = "02:30"; - LocalTime time = LocalTime.parse(timeString); - Duration duration = convertLocalTimeToDuration(time); - System.out.println("Converted Duration: " + duration); // PT2H30M - - // 포매팅 테스트 - String formattedDuration = formatDuration(duration); - System.out.println("Formatted Duration: " + formattedDuration); // 02:30 - } } diff --git a/src/test/java/org/dnd/timeet/common/utils/DurationUtilsTest.java b/src/test/java/org/dnd/timeet/common/utils/DurationUtilsTest.java new file mode 100644 index 0000000..546991f --- /dev/null +++ b/src/test/java/org/dnd/timeet/common/utils/DurationUtilsTest.java @@ -0,0 +1,21 @@ +package org.dnd.timeet.common.utils; + +import java.time.Duration; +import java.time.LocalTime; +import org.junit.jupiter.api.Test; + +public class DurationUtilsTest { + + @Test + public void test() { + // 변환 테스트 + String timeString = "02:30"; + LocalTime time = LocalTime.parse(timeString); + Duration duration = DurationUtils.convertLocalTimeToDuration(time); + System.out.println("Converted Duration: " + duration); // PT2H30M + + // 포매팅 테스트 + String formattedDuration = DurationUtils.formatDuration(duration); + System.out.println("Formatted Duration: " + formattedDuration); // 02:30 + } +} From a1a256829d13f66aa876a2f74c59b34c24a51e42 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 15 Feb 2024 12:52:11 +0900 Subject: [PATCH 58/81] [#26] feat: Add endMeeting --- .../timeet/meeting/application/MeetingService.java | 8 ++++++++ .../timeet/meeting/controller/MeetingController.java | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index 6a1e3bf..c73085f 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -42,6 +42,14 @@ public Meeting createMeeting(MeetingCreateRequest createDto, Member member) { return meeting; } + public void endMeeting(Long meetingId) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + meeting.endMeeting(); + } + public Meeting addParticipantToMeeting(Long meetingId, Member member) { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index d4ed250..937b293 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -21,6 +21,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -58,6 +59,16 @@ public ResponseEntity> attendMeeting( return ResponseEntity.ok(ApiUtils.success(meetingCreateResponse)); } + @PatchMapping("/{meeting-id}/end") + @Operation(summary = "회의 종료", description = "회의를 종료한다.") + public ResponseEntity> closeMeeting( + @PathVariable("meeting-id") Long meetingId) { + meetingService.endMeeting(meetingId); + MeetingReportInfoResponse meetingReportInfoResponse = meetingService.createReport(meetingId); + + return ResponseEntity.ok(ApiUtils.success(meetingReportInfoResponse)); + } + @GetMapping("/{id}") @Operation(summary = "단일 회의 조회", description = "지정된 id에 해당하는 회의를 조회한다.") public ResponseEntity> getTimerById(@PathVariable("id") Long meetingId) { @@ -75,6 +86,7 @@ public ResponseEntity> getMeetingReport( return ResponseEntity.ok(ApiUtils.success(meetingReportInfoResponse)); } + @DeleteMapping("/{meeting-id}") @Operation(summary = "회의 삭제", description = "지정된 id에 해당하는 회의를 삭제한다.") public ResponseEntity deleteMeeting(@PathVariable("meeting-id") Long meetingId) { From bb371e65f2951acc75e7379c11e28ce12fc94316 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 15 Feb 2024 13:01:59 +0900 Subject: [PATCH 59/81] [#26] feat: Add exception for endMeeting --- src/main/java/org/dnd/timeet/meeting/domain/Meeting.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index 32cde56..9d504de 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -26,6 +26,7 @@ import lombok.NoArgsConstructor; import org.dnd.timeet.common.domain.AuditableEntity; import org.dnd.timeet.common.exception.BadRequestError; +import org.dnd.timeet.common.exception.BadRequestError.ErrorCode; import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.participant.domain.Participant; import org.hibernate.annotations.Where; @@ -78,7 +79,7 @@ public class Meeting extends AuditableEntity { @Builder public Meeting(Member hostMember, String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, - String location, String description, Integer imgNum) { + String location, String description, Integer imgNum) { this.hostMember = hostMember; this.title = title; this.startTime = startTime; @@ -95,6 +96,12 @@ public void startMeeting() { public void endMeeting() { this.endTime = LocalDateTime.now(); + + if (this.status == MeetingStatus.COMPLETED) { + throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("MeetingId", "Meeting already completed")); + } + this.status = MeetingStatus.COMPLETED; long durationInSeconds = Duration.between(startTime, endTime).getSeconds(); From 0930b96c7e8f0f16d080705099641f2bfcb50b8c Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 15 Feb 2024 13:17:31 +0900 Subject: [PATCH 60/81] [#26] feat: Add Response DTO and Exception for exceed meeting duration --- .../meeting/controller/MeetingController.java | 12 ++++++++---- .../org/dnd/timeet/meeting/domain/Meeting.java | 8 +++++++- .../timeet/meeting/dto/MeetingReportResponse.java | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/meeting/dto/MeetingReportResponse.java diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index 937b293..28d9919 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -15,6 +15,7 @@ import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; import org.dnd.timeet.meeting.dto.MeetingReportInfoResponse; +import org.dnd.timeet.meeting.dto.MeetingReportResponse; import org.dnd.timeet.member.dto.MemberInfoListResponse; import org.dnd.timeet.member.dto.MemberInfoResponse; import org.springframework.http.ResponseEntity; @@ -61,12 +62,13 @@ public ResponseEntity> attendMeeting( @PatchMapping("/{meeting-id}/end") @Operation(summary = "회의 종료", description = "회의를 종료한다.") - public ResponseEntity> closeMeeting( + public ResponseEntity> closeMeeting( @PathVariable("meeting-id") Long meetingId) { meetingService.endMeeting(meetingId); MeetingReportInfoResponse meetingReportInfoResponse = meetingService.createReport(meetingId); + MeetingReportResponse meetingReportResponse = new MeetingReportResponse(meetingReportInfoResponse); - return ResponseEntity.ok(ApiUtils.success(meetingReportInfoResponse)); + return ResponseEntity.ok(ApiUtils.success(meetingReportResponse)); } @GetMapping("/{id}") @@ -80,11 +82,13 @@ public ResponseEntity> getTimerById(@PathVariable @GetMapping("{meeting-id}/report") @Operation(summary = "회의 리포트 조회", description = "회의 리포트를 조회한다.") - public ResponseEntity> getMeetingReport( + public ResponseEntity> getMeetingReport( @PathVariable("meeting-id") Long meetingId) { MeetingReportInfoResponse meetingReportInfoResponse = meetingService.createReport(meetingId); - return ResponseEntity.ok(ApiUtils.success(meetingReportInfoResponse)); + MeetingReportResponse meetingReportResponse = new MeetingReportResponse(meetingReportInfoResponse); + + return ResponseEntity.ok(ApiUtils.success(meetingReportResponse)); } @DeleteMapping("/{meeting-id}") diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index 9d504de..f14d5ef 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -101,10 +101,16 @@ public void endMeeting() { throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, Collections.singletonMap("MeetingId", "Meeting already completed")); } - + this.status = MeetingStatus.COMPLETED; long durationInSeconds = Duration.between(startTime, endTime).getSeconds(); + + if (durationInSeconds > 24 * 3600) { // 하루를 초과하는 경우 + throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("Meeting", "Meeting duration exceeds one day")); + } + this.totalActualDuration = LocalTime.ofSecondOfDay(durationInSeconds); } diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportResponse.java new file mode 100644 index 0000000..9e3d4ab --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingReportResponse.java @@ -0,0 +1,15 @@ +package org.dnd.timeet.meeting.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MeetingReportResponse { + + private MeetingReportInfoResponse report; + + public MeetingReportResponse(MeetingReportInfoResponse report) { + this.report = report; + } +} From fb18c42d69261f8c12cba896d6487d868f7303a9 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 15 Feb 2024 22:24:30 +0900 Subject: [PATCH 61/81] [#-] refactor: Change totalEstimatedDuration and totalActualDuration from LocalTime to Duration --- .../java/org/dnd/timeet/common/utils/TimeUtils.java | 13 ++++++------- .../java/org/dnd/timeet/meeting/domain/Meeting.java | 13 ++++++------- .../timeet/meeting/dto/MeetingCreateRequest.java | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java b/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java index c61f364..b3bad51 100644 --- a/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java +++ b/src/main/java/org/dnd/timeet/common/utils/TimeUtils.java @@ -1,24 +1,23 @@ package org.dnd.timeet.common.utils; import java.time.Duration; -import java.time.LocalTime; import java.util.Collections; import org.dnd.timeet.common.exception.InternalServerError; public class TimeUtils { - public static Duration calculateTimeDiff(LocalTime ActualDuration, LocalTime EstimatedDuration) { + public static Duration calculateTimeDiff(Duration actualDuration, Duration estimatedDuration) { - if (ActualDuration == null) { + if (actualDuration == null) { throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, Collections.singletonMap("LocalTime", "ActualDuration is null")); } - if (EstimatedDuration == null) { + if (estimatedDuration == null) { throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, - Collections.singletonMap("LocalTime", "EstimatedDuration is null")); + Collections.singletonMap("LocalTime", "estimatedDuration is null")); } - - return Duration.between(EstimatedDuration, ActualDuration); + // actualDuration - estimatedDuration + return actualDuration.minus(estimatedDuration); } public static String formatDuration(Duration duration) { diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index f14d5ef..9f219cc 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -13,7 +13,6 @@ import jakarta.persistence.Table; import java.time.Duration; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -55,11 +54,11 @@ public class Meeting extends AuditableEntity { // 예상 소요 시간 @Column(nullable = false, name = "total_estimated_duration") - private LocalTime totalEstimatedDuration; + private Duration totalEstimatedDuration; // 실제 소요 시간, 초기값 null @Column(name = "total_actual_duration") - private LocalTime totalActualDuration; + private Duration totalActualDuration; @Column(nullable = true, length = 255) private String location; @@ -78,7 +77,7 @@ public class Meeting extends AuditableEntity { private Set participants = new HashSet<>(); @Builder - public Meeting(Member hostMember, String title, LocalDateTime startTime, LocalTime totalEstimatedDuration, + public Meeting(Member hostMember, String title, LocalDateTime startTime, Duration totalEstimatedDuration, String location, String description, Integer imgNum) { this.hostMember = hostMember; this.title = title; @@ -104,14 +103,14 @@ public void endMeeting() { this.status = MeetingStatus.COMPLETED; - long durationInSeconds = Duration.between(startTime, endTime).getSeconds(); + Duration duration = Duration.between(startTime, endTime); - if (durationInSeconds > 24 * 3600) { // 하루를 초과하는 경우 + if (duration.getSeconds() > 24 * 3600) { // 하루를 초과하는 경우 throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, Collections.singletonMap("Meeting", "Meeting duration exceeds one day")); } - this.totalActualDuration = LocalTime.ofSecondOfDay(durationInSeconds); + this.totalActualDuration = duration; } public void cancelMeeting() { diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java index fa41bc5..8d088f0 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java @@ -2,12 +2,12 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.dnd.timeet.common.utils.DurationUtils; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.member.domain.Member; import org.springframework.format.annotation.DateTimeFormat; @@ -50,7 +50,7 @@ public Meeting toEntity(Member member) { .location(this.location) .startTime(startTime) .description(this.description) - .totalEstimatedDuration(this.estimatedTotalDuration) + .totalEstimatedDuration(DurationUtils.convertLocalTimeToDuration(this.estimatedTotalDuration)) .imgNum(imageNum) .build(); } From 5f77342e9dbdb478dd3be69a3db25aac1707307b Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 15 Feb 2024 22:30:58 +0900 Subject: [PATCH 62/81] [#-] refactor: Enhance: Update formatDuration method to include seconds in output --- .../dnd/timeet/agenda/dto/AgendaReportInfoResponse.java | 2 +- .../java/org/dnd/timeet/common/utils/DurationUtils.java | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java b/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java index d311ad1..d3f2125 100644 --- a/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java +++ b/src/main/java/org/dnd/timeet/agenda/dto/AgendaReportInfoResponse.java @@ -37,7 +37,7 @@ public static AgendaReportInfoResponse from(Agenda agenda) { // 매개변수로 return AgendaReportInfoResponse.builder() .agendaId(agenda.getId()) .title(agenda.getTitle()) - .diff(formatDuration(calculateTimeDiff(agenda.getActualDuration(), agenda.getEstimatedDuration()))) + .diff(formatDuration(calculateTimeDiff(agenda.getTotalDuration(), agenda.getAllocatedDuration()))) .build(); } } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java b/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java index 998fadb..699e891 100644 --- a/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java +++ b/src/main/java/org/dnd/timeet/common/utils/DurationUtils.java @@ -17,15 +17,16 @@ public static Duration convertLocalTimeToDuration(LocalTime time) { } /** - * Duration 객체를 HH:mm 형식의 문자열로 포매팅한다. + * Duration 객체를 HH:mm:ss 형식의 문자열로 포매팅한다. * * @param duration Duration 객체 - * @return 포매팅된 시간 문자열 (HH:mm) + * @return 포매팅된 시간 문자열 (HH:mm:ss) */ public static String formatDuration(Duration duration) { long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; - return String.format("%02d:%02d", hours, minutes); + long minutes = duration.toMinutesPart(); + long seconds = duration.toSecondsPart(); + return String.format("%02d:%02d:%02d", hours, minutes, seconds); } } From 0888e76ecf113dd4226098dafbf6147f43e67e13 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 15 Feb 2024 22:33:58 +0900 Subject: [PATCH 63/81] [#39] feat: Implement automatic meeting start feature via scheduling --- .../org/dnd/timeet/config/AsyncConfig.java | 9 +++++ .../dnd/timeet/config/SchedulerConfig.java | 19 ++++++++++ .../application/MeetingAsyncService.java | 37 +++++++++++++++++++ .../meeting/application/MeetingScheduler.java | 32 ++++++++++++++++ .../meeting/application/MeetingService.java | 5 +++ 5 files changed, 102 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/config/AsyncConfig.java create mode 100644 src/main/java/org/dnd/timeet/config/SchedulerConfig.java create mode 100644 src/main/java/org/dnd/timeet/meeting/application/MeetingAsyncService.java create mode 100644 src/main/java/org/dnd/timeet/meeting/application/MeetingScheduler.java diff --git a/src/main/java/org/dnd/timeet/config/AsyncConfig.java b/src/main/java/org/dnd/timeet/config/AsyncConfig.java new file mode 100644 index 0000000..c3732e3 --- /dev/null +++ b/src/main/java/org/dnd/timeet/config/AsyncConfig.java @@ -0,0 +1,9 @@ +package org.dnd.timeet.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +@Configuration +@EnableAsync +public class AsyncConfig { +} diff --git a/src/main/java/org/dnd/timeet/config/SchedulerConfig.java b/src/main/java/org/dnd/timeet/config/SchedulerConfig.java new file mode 100644 index 0000000..9383ffe --- /dev/null +++ b/src/main/java/org/dnd/timeet/config/SchedulerConfig.java @@ -0,0 +1,19 @@ +package org.dnd.timeet.config; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SchedulerConfig { + + private final int poolSize = 10; + + // 스프링 컨테이너가 종료될 때 스케줄러 작업 종료 및 스레드 풀 정리 + @Bean(destroyMethod = "shutdown") + public ScheduledExecutorService taskScheduler() { + return Executors.newScheduledThreadPool(poolSize); + } +} + diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingAsyncService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingAsyncService.java new file mode 100644 index 0000000..ca117e5 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingAsyncService.java @@ -0,0 +1,37 @@ +package org.dnd.timeet.meeting.application; + +import java.util.Collections; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.meeting.domain.Meeting; +import org.dnd.timeet.meeting.domain.MeetingRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class MeetingAsyncService { + + private final MeetingRepository meetingRepository; + private final Logger logger = LoggerFactory.getLogger(MeetingAsyncService.class); + + public MeetingAsyncService(MeetingRepository meetingRepository) { + this.meetingRepository = meetingRepository; + } + + @Transactional + @Async // 비동기 작업 실행시 발생하는 에러 처리 + public void startScheduledMeeting(Long meetingId) { + try { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + meeting.startMeeting(); + meetingRepository.save(meeting); + } catch (Exception e) { + logger.error("Error starting scheduled meeting", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingScheduler.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingScheduler.java new file mode 100644 index 0000000..854341a --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingScheduler.java @@ -0,0 +1,32 @@ +package org.dnd.timeet.meeting.application; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.BadRequestError; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; + +@EnableScheduling +@Service +@RequiredArgsConstructor +public class MeetingScheduler { + + private final MeetingAsyncService meetingAsyncService; + private final ScheduledExecutorService scheduledExecutorService; + + public void scheduleMeetingStart(Long meetingId, LocalDateTime startTime) { + long delay = ChronoUnit.MILLIS.between(LocalDateTime.now(), startTime); + if (delay < 0) { + throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED, + Collections.singletonMap("startTime", "startTime is past")); + } + + // 스케줄러 생성 + scheduledExecutorService.schedule(() -> + meetingAsyncService.startScheduledMeeting(meetingId), delay, TimeUnit.MILLISECONDS); + } +} diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index c73085f..7b02fa4 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -31,6 +31,8 @@ public class MeetingService { private final MeetingRepository meetingRepository; private final ParticipantRepository participantRepository; private final AgendaRepository agendaRepository; + private final MeetingScheduler meetingScheduler; + public Meeting createMeeting(MeetingCreateRequest createDto, Member member) { Meeting meeting = createDto.toEntity(member); @@ -39,6 +41,9 @@ public Meeting createMeeting(MeetingCreateRequest createDto, Member member) { Participant participant = new Participant(meeting, member); participantRepository.save(participant); + // 스케줄러를 통해 회의 시작 시간에 회의 시작 + meetingScheduler.scheduleMeetingStart(meeting.getId(), meeting.getStartTime()); + return meeting; } From d691ed6a6c892de7c1fd0696e18356625e4d4891 Mon Sep 17 00:00:00 2001 From: Starlight258 Date: Thu, 15 Feb 2024 22:35:36 +0900 Subject: [PATCH 64/81] [#-] feat: Extend MeetingInfoResponse with additional meeting details --- .../meeting/dto/MeetingInfoResponse.java | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java index 39e9080..9060097 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java @@ -1,9 +1,11 @@ package org.dnd.timeet.meeting.dto; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Duration; import lombok.Builder; import lombok.Getter; import lombok.Setter; +import org.dnd.timeet.common.utils.DurationUtils; import org.dnd.timeet.meeting.domain.Meeting; @Schema(description = "회의 정보 응답") @@ -17,22 +19,48 @@ public class MeetingInfoResponse { @Schema(description = "회의 제목", example = "2차 회의") private String title; - @Schema(description = "회의 목표", example = "2개의 사안 모두 해결하기") + @Schema(description = "회의 공지사항", example = "2개의 사안 모두 해결하기") private String description; + @Schema(description = "회의 상태", example = "SCHEDULED") + private String meetingStatus; + + @Schema(description = "회의 방장 멤버 ID", example = "13") + private Long hostMemberId; + + @Schema(description = "회의 시작 일자", example = "2024-01-11T13:20") + private String startTime; + + @Schema(description = "예상 소요시간", example = "03:00:00") + private String totalEstimatedDuration; + + @Schema(description = "썸네일 이미지 번호", example = "1") + private Integer imgNum; + @Builder - public MeetingInfoResponse(Long meetingId, String title, String description) { + public MeetingInfoResponse(Long meetingId, String title, String description, String meetingStatus, + Long hostMemberId, + String startTime, Duration totalEstimatedDuration, Integer imgNum) { this.meetingId = meetingId; this.title = title; this.description = description; + this.meetingStatus = meetingStatus; + this.hostMemberId = hostMemberId; + this.startTime = startTime; + this.totalEstimatedDuration = DurationUtils.formatDuration(totalEstimatedDuration); + this.imgNum = imgNum; } - public static MeetingInfoResponse from(Meeting meeting) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 return MeetingInfoResponse.builder() .meetingId(meeting.getId()) .title(meeting.getTitle()) .description(meeting.getDescription()) + .meetingStatus(meeting.getStatus().name()) + .hostMemberId(meeting.getHostMember().getId()) + .startTime(meeting.getStartTime().toString()) + .totalEstimatedDuration(meeting.getTotalEstimatedDuration()) + .imgNum(meeting.getImgNum()) .build(); } } \ No newline at end of file From f80cd783a0c82d18571b3e58b16aa911d5a02774 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 15 Feb 2024 23:11:19 +0900 Subject: [PATCH 65/81] [#22] feat: Add patch fcmToken controller --- .../common/security/annotation/ReqUser.java | 14 ++++++++++++++ .../fcm/application/FCMNotificationService.java | 15 +++++---------- .../timeet/member/application/MemberService.java | 14 ++++++-------- .../member/controller/MemberController.java | 13 +++++++++++++ .../org/dnd/timeet/member/domain/Member.java | 1 + .../timeet/member/dto/RegisterFcmRequest.java | 16 ++++++++++++++++ 6 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java create mode 100644 src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java diff --git a/src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java b/src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java new file mode 100644 index 0000000..07c8819 --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java @@ -0,0 +1,14 @@ +package org.dnd.timeet.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") +public @interface ReqUser { + +} diff --git a/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java index 556d75b..ba76173 100644 --- a/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java +++ b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java @@ -4,7 +4,6 @@ import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import java.util.Collections; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.InternalServerError; import org.dnd.timeet.common.exception.NotFoundError; @@ -21,14 +20,10 @@ public class FCMNotificationService { private final MemberRepository memberRepository; public void sendNotificationByToken(FCMNotificationRequestDto requestDto) { - Optional member = memberRepository.findById(requestDto.getTargetMemberId()); - - if (member.isEmpty()) { - throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("MemberId", "Member not found")); - } - - if (member.get().getFcmToken() == null) { + Member member = memberRepository.findById(requestDto.getTargetMemberId()) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found"))); + if (member.getFcmToken() == null) { throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("fcmToken", "fcmToken not exist")); } @@ -39,7 +34,7 @@ public void sendNotificationByToken(FCMNotificationRequestDto requestDto) { .build(); Message message = Message.builder() - .setToken(member.get().getFcmToken()) + .setToken(member.getFcmToken()) .setNotification(notification) .build(); diff --git a/src/main/java/org/dnd/timeet/member/application/MemberService.java b/src/main/java/org/dnd/timeet/member/application/MemberService.java index 5912a34..e0c6910 100644 --- a/src/main/java/org/dnd/timeet/member/application/MemberService.java +++ b/src/main/java/org/dnd/timeet/member/application/MemberService.java @@ -2,7 +2,6 @@ import java.util.Collections; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.NotFoundError; import org.dnd.timeet.member.domain.Member; @@ -19,14 +18,13 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; + @Transactional public void upsertFcmToken(Long id, String fcmToken) { - Optional member = memberRepository.findById(id); - if (member.isPresent()) { - member.get().setFcmToken(fcmToken); - } else { - throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("MemberId", "Member not found")); - } + Member member = memberRepository.findById(id) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found"))); + member.setFcmToken(fcmToken); + } } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/controller/MemberController.java b/src/main/java/org/dnd/timeet/member/controller/MemberController.java index 6065921..5168434 100644 --- a/src/main/java/org/dnd/timeet/member/controller/MemberController.java +++ b/src/main/java/org/dnd/timeet/member/controller/MemberController.java @@ -1,8 +1,14 @@ package org.dnd.timeet.member.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.security.annotation.ReqUser; import org.dnd.timeet.member.application.MemberService; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.dto.RegisterFcmRequest; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,5 +20,12 @@ public class MemberController { private final MemberService memberService; + @PatchMapping + @Operation(summary = "fcmToken 등록", description = "fcmToken을 등록한다.") + public void registerFcmToken(@RequestBody RegisterFcmRequest registerFcmRequest, + @ReqUser Member member) { + memberService.upsertFcmToken(member.getId(), registerFcmRequest.getFcmToken()); + + } } diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 557176d..3a865be 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -52,6 +52,7 @@ public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth this.provider = provider; } + public void setFcmToken(String fcmToken) { this.fcmToken = fcmToken; } diff --git a/src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java b/src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java new file mode 100644 index 0000000..61594c8 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java @@ -0,0 +1,16 @@ +package org.dnd.timeet.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Schema(description = "fcmToken 등록 요청") +@Getter +@Setter +@NoArgsConstructor +public class RegisterFcmRequest { + + @Schema(description = "fcmToken", nullable = false, example = "fcmToken") + private String fcmToken; +} From 696a97d82ec91d9ef0131cc80191d894e18fd6b3 Mon Sep 17 00:00:00 2001 From: Mint Date: Fri, 16 Feb 2024 00:15:50 +0900 Subject: [PATCH 66/81] [#35] feat: Add real-time remainig time tracking for meetings using WebSocket --- .../meeting/application/MeetingService.java | 11 ++++++ .../meeting/controller/MeetingController.java | 14 +++++++ .../dnd/timeet/meeting/domain/Meeting.java | 35 +++++++++++++++++ .../meeting/dto/MeetingInfoResponse.java | 8 +++- .../dto/MeetingRemainingTimeResponse.java | 39 +++++++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/dnd/timeet/meeting/dto/MeetingRemainingTimeResponse.java diff --git a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java index 7b02fa4..2eddba0 100644 --- a/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java +++ b/src/main/java/org/dnd/timeet/meeting/application/MeetingService.java @@ -13,9 +13,11 @@ import org.dnd.timeet.agenda.dto.AgendaReportInfoResponse; import org.dnd.timeet.common.exception.BadRequestError; import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.common.exception.NotFoundError.ErrorCode; import org.dnd.timeet.meeting.domain.Meeting; import org.dnd.timeet.meeting.domain.MeetingRepository; import org.dnd.timeet.meeting.dto.MeetingCreateRequest; +import org.dnd.timeet.meeting.dto.MeetingRemainingTimeResponse; import org.dnd.timeet.meeting.dto.MeetingReportInfoResponse; import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.participant.domain.Participant; @@ -138,4 +140,13 @@ public List getMeetingMembers(Long meetingId) { .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public MeetingRemainingTimeResponse getRemainingTime(Long meetingId) { + Meeting meeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MeetingId", "Meeting not found"))); + + return MeetingRemainingTimeResponse.from(meeting); + } + } diff --git a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java index 28d9919..3845e24 100644 --- a/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java +++ b/src/main/java/org/dnd/timeet/meeting/controller/MeetingController.java @@ -14,11 +14,15 @@ import org.dnd.timeet.meeting.dto.MeetingCreateRequest; import org.dnd.timeet.meeting.dto.MeetingCreateResponse; import org.dnd.timeet.meeting.dto.MeetingInfoResponse; +import org.dnd.timeet.meeting.dto.MeetingRemainingTimeResponse; import org.dnd.timeet.meeting.dto.MeetingReportInfoResponse; import org.dnd.timeet.meeting.dto.MeetingReportResponse; import org.dnd.timeet.member.dto.MemberInfoListResponse; import org.dnd.timeet.member.dto.MemberInfoResponse; import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -80,6 +84,16 @@ public ResponseEntity> getTimerById(@PathVariable return ResponseEntity.ok(ApiUtils.success(meetingInfoResponse)); } + @Operation(summary = "남은 시간 조회", description = "웹소켓을 통해 특정 회의의 남은 시간을 조회한다.") + @MessageMapping("/meeting/{meeting-id}/remaining-time") + @SendTo("/topic/meeting/{meeting-id}/remaining-time") + public ResponseEntity> getRemainingTime( + @DestinationVariable("meeting-id") Long meetingId) { + MeetingRemainingTimeResponse response = meetingService.getRemainingTime(meetingId); + + return ResponseEntity.ok(ApiUtils.success(response)); + } + @GetMapping("{meeting-id}/report") @Operation(summary = "회의 리포트 조회", description = "회의 리포트를 조회한다.") public ResponseEntity> getMeetingReport( diff --git a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java index 9f219cc..260d898 100644 --- a/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java +++ b/src/main/java/org/dnd/timeet/meeting/domain/Meeting.java @@ -150,6 +150,41 @@ public void assignNewHostRandomly() { this.assignHostMember(newHost); } + // 현재까지 진행된 시간 계산 + public Duration calculateCurrentDuration() { + switch (this.status) { + case SCHEDULED: + return Duration.ZERO; + case INPROGRESS: + return Duration.between(this.startTime, LocalDateTime.now()); + case COMPLETED: + return this.totalActualDuration; + case CANCELED: + throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("Meeting", "Meeting is CANCELED")); + default: + throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("Meeting", "MeetingStatus is not valid")); + } + } + + // 남은 시간 계산 메서드 + public Duration calculateRemainingTime() { + switch (this.status) { + case SCHEDULED: + return this.totalEstimatedDuration; + case INPROGRESS: + return this.totalEstimatedDuration.minus(calculateCurrentDuration()); + case COMPLETED: + return Duration.ZERO; + case CANCELED: + throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("Meeting", "Meeting is CANCELED")); + default: + throw new BadRequestError(ErrorCode.WRONG_REQUEST_TRANSMISSION, + Collections.singletonMap("Meeting", "MeetingStatus is not valid")); + } + } } diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java index 9060097..43310db 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingInfoResponse.java @@ -34,13 +34,17 @@ public class MeetingInfoResponse { @Schema(description = "예상 소요시간", example = "03:00:00") private String totalEstimatedDuration; + @Schema(description = "회의 남은 시간", example = "00:03:00") + private String remainingTime; + @Schema(description = "썸네일 이미지 번호", example = "1") private Integer imgNum; @Builder public MeetingInfoResponse(Long meetingId, String title, String description, String meetingStatus, Long hostMemberId, - String startTime, Duration totalEstimatedDuration, Integer imgNum) { + String startTime, Duration totalEstimatedDuration, String remainingTime, + Integer imgNum) { this.meetingId = meetingId; this.title = title; this.description = description; @@ -48,6 +52,7 @@ public MeetingInfoResponse(Long meetingId, String title, String description, Str this.hostMemberId = hostMemberId; this.startTime = startTime; this.totalEstimatedDuration = DurationUtils.formatDuration(totalEstimatedDuration); + this.remainingTime = remainingTime; this.imgNum = imgNum; } @@ -60,6 +65,7 @@ public static MeetingInfoResponse from(Meeting meeting) { // 매개변수로부 .hostMemberId(meeting.getHostMember().getId()) .startTime(meeting.getStartTime().toString()) .totalEstimatedDuration(meeting.getTotalEstimatedDuration()) + .remainingTime(DurationUtils.formatDuration(meeting.calculateRemainingTime())) .imgNum(meeting.getImgNum()) .build(); } diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingRemainingTimeResponse.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingRemainingTimeResponse.java new file mode 100644 index 0000000..e4a9ce2 --- /dev/null +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingRemainingTimeResponse.java @@ -0,0 +1,39 @@ +package org.dnd.timeet.meeting.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.dnd.timeet.common.utils.DurationUtils; +import org.dnd.timeet.meeting.domain.Meeting; + +@Schema(description = "회의 정보 응답") +@Getter +@Setter +public class MeetingRemainingTimeResponse { + + @Schema(description = "회의 id", example = "12L") + private Long meetingId; + + @Schema(description = "회의 상태", example = "SCHEDULED") + private String meetingStatus; + + @Schema(description = "회의 남은 시간", example = "00:03:00") + private String remainingTime; + + @Builder + public MeetingRemainingTimeResponse(Long meetingId, String meetingStatus, String remainingTime) { + this.meetingId = meetingId; + this.meetingStatus = meetingStatus; + this.remainingTime = remainingTime; + } + + public static MeetingRemainingTimeResponse from(Meeting meeting) { // 매개변수로부터 객체를 생성하는 팩토리 메서드 + return MeetingRemainingTimeResponse + .builder() + .meetingId(meeting.getId()) + .meetingStatus(meeting.getStatus().name()) + .remainingTime(DurationUtils.formatDuration(meeting.calculateRemainingTime())) + .build(); + } +} \ No newline at end of file From d2b37979c547c950a916d2a3043b87ee58cbb4f4 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 14:55:27 +0900 Subject: [PATCH 67/81] [#38] feat: Add deploy workflow using docker compose and ssh --- .../workflows/{build.yml => build-deploy.yml} | 51 +++++++++++++++++- .github/workflows/deploy.yml | 54 ------------------- Dockerfile | 4 +- 3 files changed, 53 insertions(+), 56 deletions(-) rename .github/workflows/{build.yml => build-deploy.yml} (52%) delete mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build-deploy.yml similarity index 52% rename from .github/workflows/build.yml rename to .github/workflows/build-deploy.yml index dcdb8a3..4d6b39f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build-deploy.yml @@ -1,6 +1,6 @@ name: Build -on: [pull_request, workflow_dispatch] +on: [ pull_request, workflow_dispatch ] jobs: @@ -43,3 +43,52 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} run: ./gradlew clean build sonar --info + + docker: + runs-on: ubuntu-latest + steps: + # repository checkout + - uses: actions/checkout@v2 + + - name: Set output + id: vars + run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} + + - name: Docker Setup QEMU + uses: docker/setup-qemu-action@v1.2.0 + + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v1.6.0 + + - name: Docker Login + uses: docker/login-action@v1.10.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build Image + working-directory: . + run: docker buildx build --platform linux/amd64,linux/arm64 -t syw5141/dnd-10th-2-backend:latest --push . + + + deploy: + needs: docker + name: Deploy + runs-on: ubuntu-latest + steps: + - name: Set output + id: vars + run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} + + - name: Deploy on rpi4 server using docker + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + port: ${{ secrets.PORT }} + script: | + cd ~/dnd-10th-2-backend + docker-compose pull + docker-compose up --force-recreate --build -d + docker image prune -f diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 04c3093..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Deploy to ECR - -on: - push: - branches: - - main - tags: - - v* - workflow_dispatch: - -jobs: - build: - name: Build Image - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v2 - - - name: Set RELEASE_VERSION - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - run: | - docker build -t dnd-10th-2-backend-springboot . - docker tag dnd-10th-2-backend-springboot:latest public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest - docker push public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: public.ecr.aws/g8f7f0e2/dnd-10th-2-backend:latest - image: ${{ steps.build-image.outputs.image }} - - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: dnd-cluster-service - cluster: dnd-cluster - wait-for-service-stability: true - diff --git a/Dockerfile b/Dockerfile index 5ff665d..529ebe5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # JDK 17을 기반으로 하는 이미지 사용 -FROM openjdk:17-alpine +FROM openjdk:17-oracle # 작업 디렉토리 WORKDIR /usr/src/app @@ -13,5 +13,7 @@ COPY ${JAR_PATH}/*.jar ${JAR_PATH}/app.jar # 환경변수 설정 (실행중인 컨테이너에 액세스 가능) ENV JAR_PATH ${JAR_PATH} +EXPOSE 8080 + # ENV 이용해서 실행 CMD java -jar ${JAR_PATH}/app.jar From 5d8eacfdb44f00da91df61e42975f1d8feb94ce2 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 16:11:20 +0900 Subject: [PATCH 68/81] [#38] feat: Fix workflow for build and docker sequence --- .github/workflows/build-deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 21fcc94..85661a4 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -49,6 +49,7 @@ jobs: run: ./gradlew clean build sonar --info docker: + needs: build runs-on: ubuntu-latest steps: # repository checkout From d0be250fbd3d33909a9bdcf6d3db7a8c81f97fed Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 16:27:37 +0900 Subject: [PATCH 69/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 85661a4..5ab2959 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -72,7 +72,7 @@ jobs: password: ${{ secrets.DOCKER_TOKEN }} - name: Build Image - working-directory: . + working-directory: ./dnd-10th-2-backend run: docker buildx build --platform linux/amd64,linux/arm64 -t syw5141/dnd-10th-2-backend:latest --push . From 26dafd95e1107e89169e9456753dced0f3e552c1 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 16:34:34 +0900 Subject: [PATCH 70/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 5ab2959..5a980e4 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -46,7 +46,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: ./gradlew clean build sonar --info + run: ./gradlew clean sonar --info docker: needs: build @@ -55,6 +55,9 @@ jobs: # repository checkout - uses: actions/checkout@v2 + - name: Build Project with Gradle + run: ./gradlew clean build + - name: Set output id: vars run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} @@ -71,9 +74,13 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Build Image - working-directory: ./dnd-10th-2-backend - run: docker buildx build --platform linux/amd64,linux/arm64 -t syw5141/dnd-10th-2-backend:latest --push . + - name: Build and Push Docker Image + run: | + JAR_FILE=$(ls ./build/libs/*.jar | head -n 1) + docker buildx build --platform linux/amd64,linux/arm64 \ + -t syw5141/dnd-10th-2-backend:latest \ + --build-arg JAR_FILE="$JAR_FILE" \ + --push . deploy: From a103dcd66820630c54999c29f2fba2468f496f6f Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 16:38:39 +0900 Subject: [PATCH 71/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 5a980e4..4299f18 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -46,7 +46,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: ./gradlew clean sonar --info + run: ./gradlew clean build sonar --info docker: needs: build @@ -55,9 +55,6 @@ jobs: # repository checkout - uses: actions/checkout@v2 - - name: Build Project with Gradle - run: ./gradlew clean build - - name: Set output id: vars run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} From e10b32a333da2ff22e9be29bca05f0e3d3541f62 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 16:43:07 +0900 Subject: [PATCH 72/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 4299f18..5233fb2 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -73,7 +73,7 @@ jobs: - name: Build and Push Docker Image run: | - JAR_FILE=$(ls ./build/libs/*.jar | head -n 1) + JAR_FILE=$(ls /usr/src/app/build/libs/*.jar | head -n 1) docker buildx build --platform linux/amd64,linux/arm64 \ -t syw5141/dnd-10th-2-backend:latest \ --build-arg JAR_FILE="$JAR_FILE" \ From 9261930338471efdac9043e20e6b67e48ea65ce8 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 16:55:15 +0900 Subject: [PATCH 73/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 5233fb2..032961d 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -73,7 +73,7 @@ jobs: - name: Build and Push Docker Image run: | - JAR_FILE=$(ls /usr/src/app/build/libs/*.jar | head -n 1) + JAR_FILE=$(ls ${GITHUB_WORKSPACE}/build/libs/*.jar | head -n 1) docker buildx build --platform linux/amd64,linux/arm64 \ -t syw5141/dnd-10th-2-backend:latest \ --build-arg JAR_FILE="$JAR_FILE" \ From 05eaf8a1d175aa72bddafe90cd0dcde93c822091 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 17:00:34 +0900 Subject: [PATCH 74/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 032961d..84a19c4 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -48,6 +48,14 @@ jobs: SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} run: ./gradlew clean build sonar --info + - name: Verify JAR File Exists + run: | + ls -l ./build/libs/ + if [ -z "$(ls -A ./build/libs/*.jar)" ]; then + echo "No JAR files found in ./build/libs directory." + exit 1 + fi + docker: needs: build runs-on: ubuntu-latest @@ -73,7 +81,7 @@ jobs: - name: Build and Push Docker Image run: | - JAR_FILE=$(ls ${GITHUB_WORKSPACE}/build/libs/*.jar | head -n 1) + JAR_FILE=$(ls ./build/libs/*.jar | head -n 1) docker buildx build --platform linux/amd64,linux/arm64 \ -t syw5141/dnd-10th-2-backend:latest \ --build-arg JAR_FILE="$JAR_FILE" \ From f1f832f61d86a281fb2df0dee36efb3c601a28e4 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 17:07:15 +0900 Subject: [PATCH 75/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 84a19c4..440779e 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -4,8 +4,8 @@ on: [ pull_request, workflow_dispatch ] jobs: - build: - name: Build + style: + name: CheckStyle runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -56,17 +56,37 @@ jobs: exit 1 fi - docker: - needs: build + build: runs-on: ubuntu-latest steps: # repository checkout - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Run Checkstyle + run: ./gradlew checkstyleMain checkstyleTest + - name: Make firebase-adminsdk.json + run: | + touch src/main/resources/timeet-firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" | base64 --decode > src/main/resources/timeet-firebase-adminsdk.json + - name: Make application-prod.yml + run: | + touch src/main/resources/application-prod.yml + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > src/main/resources/application-prod.yml + - name: Make application-test.yml + run: | + touch src/main/resources/application-test.yml + echo "${{ secrets.APPLICATION_TEST }}" | base64 --decode > src/main/resources/application-test.yml - name: Set output id: vars run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} + - name: Build and analyze + run: ./gradlew clean build --info + - name: Docker Setup QEMU uses: docker/setup-qemu-action@v1.2.0 From 7b694ebb6ca95073901b6ebb2a5849963b50f1c4 Mon Sep 17 00:00:00 2001 From: Yong woo Song Date: Fri, 16 Feb 2024 17:09:16 +0900 Subject: [PATCH 76/81] chore: Fix workflow for build directory path --- .github/workflows/build-deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 440779e..68085bf 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -66,6 +66,7 @@ jobs: uses: actions/setup-java@v1 with: java-version: 17 + - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest - name: Make firebase-adminsdk.json @@ -109,7 +110,7 @@ jobs: deploy: - needs: docker + needs: build name: Deploy runs-on: ubuntu-latest steps: From 59f45383d0422ee33a9140caf8b525f9cc7a0f75 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 17:19:35 +0900 Subject: [PATCH 77/81] [#38] feat: Fix workflow for build directory --- .github/workflows/build-deploy.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 440779e..f19ca0f 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -66,6 +66,7 @@ jobs: uses: actions/setup-java@v1 with: java-version: 17 + - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest - name: Make firebase-adminsdk.json @@ -80,6 +81,11 @@ jobs: run: | touch src/main/resources/application-test.yml echo "${{ secrets.APPLICATION_TEST }}" | base64 --decode > src/main/resources/application-test.yml + + - name: Make application-local.yml + run: | + touch src/main/resources/application-local.yml + echo "${{ secrets.APPLICATION_LOCAL }}" | base64 --decode > src/main/resources/application-local.yml - name: Set output id: vars run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} From aa4928111a5951680c88f3688851aebb0441ff42 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 16 Feb 2024 17:31:08 +0900 Subject: [PATCH 78/81] [#38] feat: Fix workflow for build directory --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d963877..db3f5ce 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,4 @@ spring: profiles: active: - - local + - prod From 04ab016c959ecaea41d2591494bdcc730710e7a3 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sat, 17 Feb 2024 20:49:50 +0900 Subject: [PATCH 79/81] [#38] feat: Update workflow for separating jobs --- .github/workflows/build-deploy.yml | 59 +++--------------------------- .github/workflows/check.yml | 57 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index fc18d5f..e59ba15 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -1,61 +1,12 @@ name: Build -on: [ pull_request, workflow_dispatch ] - +on: + push: + branches: + - main + workflow_dispatch: jobs: - style: - name: CheckStyle - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 - uses: actions/setup-java@v1 - with: - java-version: 17 - - name: Run Checkstyle - run: ./gradlew checkstyleMain checkstyleTest - - name: Make firebase-adminsdk.json - run: | - touch src/main/resources/timeet-firebase-adminsdk.json - echo "${{ secrets.FIREBASE_ADMINSDK }}" | base64 --decode > src/main/resources/timeet-firebase-adminsdk.json - - name: Make application-prod.yml - run: | - touch src/main/resources/application-prod.yml - echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > src/main/resources/application-prod.yml - - name: Make application-test.yml - run: | - touch src/main/resources/application-test.yml - echo "${{ secrets.APPLICATION_TEST }}" | base64 --decode > src/main/resources/application-test.yml - - name: Cache SonarQube packages - uses: actions/cache@v1 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache Gradle packages - uses: actions/cache@v1 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: ${{ runner.os }}-gradle - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: ./gradlew clean build sonar --info - - - name: Verify JAR File Exists - run: | - ls -l ./build/libs/ - if [ -z "$(ls -A ./build/libs/*.jar)" ]; then - echo "No JAR files found in ./build/libs directory." - exit 1 - fi - build: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..90fcb00 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,57 @@ +name: check + +on: [ pull_request, workflow_dispatch ] + + +jobs: + style: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Run Checkstyle + run: ./gradlew checkstyleMain checkstyleTest + - name: Make firebase-adminsdk.json + run: | + touch src/main/resources/timeet-firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" | base64 --decode > src/main/resources/timeet-firebase-adminsdk.json + - name: Make application-prod.yml + run: | + touch src/main/resources/application-prod.yml + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > src/main/resources/application-prod.yml + - name: Make application-test.yml + run: | + touch src/main/resources/application-test.yml + echo "${{ secrets.APPLICATION_TEST }}" | base64 --decode > src/main/resources/application-test.yml + - name: Cache SonarQube packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: ./gradlew clean build sonar --info + + - name: Verify JAR File Exists + run: | + ls -l ./build/libs/ + if [ -z "$(ls -A ./build/libs/*.jar)" ]; then + echo "No JAR files found in ./build/libs directory." + exit 1 + fi \ No newline at end of file From 559f4d9ee90418cbf4b68938009b223ca1cbf535 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sat, 17 Feb 2024 21:31:58 +0900 Subject: [PATCH 80/81] [#38] feat: Update docker-compose for deploying mysql --- .gitignore | 1 + docker-compose.yml | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f8fb2e8..582167e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env timeet-firebase-adminsdk.json HELP.md .gradle diff --git a/docker-compose.yml b/docker-compose.yml index 532c217..ca28079 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,25 @@ version: '3.7' services: + db: + image: mysql:5.7 + restart: always + environment: + MYSQL_DATABASE: '{MYSQL_DATABASE}' + MYSQL_USER: '${MYSQL_USER}' + MYSQL_PASSWORD: '${MYSQL_PASSWORD}' + MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASSWORD}' + ports: + - '3306:3306' + volumes: + - db_data:/var/lib/mysql + app: - build: - context: . - dockerfile: Dockerfile - environment: # 환경 변수 설정 - - SPRING_DATASOURCE_URL=jdbc:h2:file:/usr/src/app/data/mydb;DB_CLOSE_ON_EXIT=FALSE - platform: linux/amd64 + image: syw5141/dnd-10th-2-backend:latest ports: - "8080:8080" - # h2data 볼륨을 컨테이너 내부의 /usr/src/app/data에 매핑 - volumes: - - h2data:/usr/src/app/data + depends_on: + - db # 볼륨 정의 (기본 설정 사용) volumes: - h2data: \ No newline at end of file + db_data: \ No newline at end of file From 2249a5c4b35f8eef1b4b6b1468150abc843c3527 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sat, 17 Feb 2024 22:52:10 +0900 Subject: [PATCH 81/81] [#42] feat: Update mysql8 for arm64 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ca28079..84345a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.7' services: db: - image: mysql:5.7 + image: mysql:8-oracle restart: always environment: MYSQL_DATABASE: '{MYSQL_DATABASE}'