diff --git a/build.gradle b/build.gradle index d4abb293..d5731f77 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-mongodb:3.1.8' // Spring Boot WebSocket implementation 'org.springframework.boot:spring-boot-starter-websocket:3.1.8' + // Flyway + implementation 'org.flywaydb:flyway-core:10.8.1' + implementation 'org.flywaydb:flyway-mysql:10.8.1' // SpringBoot Test testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.8' diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/ChatSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/ChatSwagger.java index 14c18345..f8ba7a5d 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/ChatSwagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/ChatSwagger.java @@ -80,4 +80,28 @@ ResponseEntity> getChatData( @Parameter(in = ParameterIn.PATH) String chatRoomId, @Parameter(hidden = true) Long userId, @Parameter(hidden = true) CursorPaginationInfoReq pageable); + + @Operation(summary = "채팅방 나가기", description = "채팅방에 참여중인 사용자가 채팅방을 나가기 위해 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "채팅방 나가기 성공"), + @ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E110102", value = SwaggerChatErrorExamples.CHAT_ROOM_USER_CANNOT_DUPLICATE) + )), + @ApiResponse(responseCode = "404", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND), + @ExampleObject(name = "E110301", value = SwaggerChatErrorExamples.CHAT_ROOM_NOT_FOUND) + } + )) + }) + ResponseEntity leaveChatRoom( + @Parameter(hidden = true) Long userId, + @Parameter(in = ParameterIn.PATH) String chatRoomId + ); } diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java index 299d3195..6794ff73 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/NotificationSwagger.java @@ -4,13 +4,16 @@ import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerNotificationErrorExamples; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; +import io.oeid.mogakgo.domain.notification.application.dto.res.NotificationCheckedRes; import io.oeid.mogakgo.domain.notification.presentation.dto.req.FCMTokenApiRequest; import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; import io.oeid.mogakgo.exception.dto.ErrorResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -51,4 +54,18 @@ ResponseEntity manageFCMToken(@Parameter(hidden = true) Long userId, ResponseEntity> getByUserId( @Parameter(hidden = true) Long id, @Parameter(hidden = true) CursorPaginationInfoReq pageable); + + @Operation(summary = "알림 확인", description = "회원의 알림을 확인할 때 사용하는 API") + @ApiResponse(responseCode = "200", description = "알림 확인 성공") + @ApiResponse(responseCode = "404", description = "요청한 유저가 존재하지 않음", content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND), + @ExampleObject(name = "E060302", value = SwaggerNotificationErrorExamples.NOTIFICATION_NOT_FOUND) + }) + ) + ResponseEntity markCheckedNotification( + @Parameter(hidden = true) Long userId, + @Parameter(in = ParameterIn.PATH) Long notificationId); } diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/ReviewSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/ReviewSwagger.java new file mode 100644 index 00000000..4c38ff0e --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/ReviewSwagger.java @@ -0,0 +1,53 @@ +package io.oeid.mogakgo.common.swagger.template; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProjectErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerReviewErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; +import io.oeid.mogakgo.domain.review.presentation.dto.req.ReviewCreateApiReq; +import io.oeid.mogakgo.domain.review.presentation.dto.res.ReviewCreateApiRes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +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 org.springframework.http.ResponseEntity; + +@Tag(name = "Review", description = "리뷰 관련 API") +@SuppressWarnings("unused") +public interface ReviewSwagger { + + @Operation(summary = "리뷰 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "리뷰 생성 성공", + content = @Content(schema = @Schema(implementation = ReviewCreateApiRes.class))), + @ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ResponseEntity.class), + examples = { + @ExampleObject(name = "E120101", value = SwaggerReviewErrorExamples.REVIEW_SENDER_OR_RECEIVER_NOT_FOUND), + @ExampleObject(name = "E120102", value = SwaggerReviewErrorExamples.REVIEW_USER_DUPLICATED), + @ExampleObject(name = "E120103", value = SwaggerReviewErrorExamples.REVIEW_PROJECT_NOT_NULL), + @ExampleObject(name = "E120104", value = SwaggerReviewErrorExamples.REVIEW_ALREADY_EXISTS), + @ExampleObject(name = "E120105", value = SwaggerReviewErrorExamples.REVIEW_USER_NOT_MATCH), + @ExampleObject(name = "E120106", value = SwaggerReviewErrorExamples.REVIEW_RATING_INVALID), + })), + @ApiResponse(responseCode = "404", description = "해당 데이터가 존재하지 않음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ResponseEntity.class), + examples = { + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND), + @ExampleObject(name = "E030301", value = SwaggerProjectErrorExamples.PROJECT_NOT_FOUND) + } + )), + }) + ResponseEntity createReviewApi( + @Parameter(hidden = true) Long userId, + ReviewCreateApiReq request); +} diff --git a/src/main/java/io/oeid/mogakgo/common/swagger/template/UserSwagger.java b/src/main/java/io/oeid/mogakgo/common/swagger/template/UserSwagger.java index a2836b2f..fca175b7 100644 --- a/src/main/java/io/oeid/mogakgo/common/swagger/template/UserSwagger.java +++ b/src/main/java/io/oeid/mogakgo/common/swagger/template/UserSwagger.java @@ -2,10 +2,14 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerAchievementErrorExamples; +import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserAchievementErrorExamples; import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples; import io.oeid.mogakgo.domain.user.application.dto.res.UserJandiRateRes; +import io.oeid.mogakgo.domain.user.presentation.dto.req.UserAchievementUpdateApiRequest; import io.oeid.mogakgo.domain.user.presentation.dto.req.UserSignUpApiReq; import io.oeid.mogakgo.domain.user.presentation.dto.req.UserUpdateApiReq; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserAchievementUpdateApiResponse; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserDevelopLanguageApiRes; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserMatchingStatus; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; @@ -124,5 +128,37 @@ ResponseEntity userUpdateApi(@Parameter(hidden = true) Long us examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND))), }) ResponseEntity userJandiRateApi(@Parameter(in = ParameterIn.PATH) Long userId); - + + @Operation(summary = "사용자의 대표 업적 변경", description = "사용자가 자신의 대표 업적을 변경하고 싶을 때 사용하는 API") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "대표 업적 변경 성공"), + @ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E140101", value = SwaggerUserAchievementErrorExamples.NON_ACHIEVED_USER_ACHIEVEMENT), + @ExampleObject(name = "E140102", value = SwaggerUserAchievementErrorExamples.ACHIEVEMENT_SHOULD_BE_DIFFERENT) + } + )), + @ApiResponse(responseCode = "403", description = "사용자의 대표 업적을 변경할 권한이 없음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = @ExampleObject(name = "E020201", value = SwaggerUserErrorExamples.USER_FORBIDDEN_OPERATION) + )), + @ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음", + content = @Content( + mediaType = APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject(name = "E130301", value = SwaggerAchievementErrorExamples.ACHIEVEMENT_NOT_FOUND), + @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND) + } + )) + }) + ResponseEntity updateUserMainAchievement( + @Parameter(hidden = true) Long userId, + UserAchievementUpdateApiRequest request + ); } diff --git a/src/main/java/io/oeid/mogakgo/core/configuration/WebSocketConfig.java b/src/main/java/io/oeid/mogakgo/core/configuration/WebSocketConfig.java index 311a7210..0eb77ffa 100644 --- a/src/main/java/io/oeid/mogakgo/core/configuration/WebSocketConfig.java +++ b/src/main/java/io/oeid/mogakgo/core/configuration/WebSocketConfig.java @@ -1,21 +1,34 @@ package io.oeid.mogakgo.core.configuration; +import io.oeid.mogakgo.domain.chat.interceptor.ChatInterceptor; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.messaging.simp.config.ChannelRegistration; +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 @RequiredArgsConstructor -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private final WebSocketHandler webSocketHandler; + private final ChatInterceptor chatInterceptor; @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*"); + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/topic", "/queue"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/chat").setAllowedOriginPatterns("*"); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(chatInterceptor); } } diff --git a/src/main/java/io/oeid/mogakgo/core/properties/JwtProperties.java b/src/main/java/io/oeid/mogakgo/core/properties/JwtProperties.java index b85900e9..44be46b7 100644 --- a/src/main/java/io/oeid/mogakgo/core/properties/JwtProperties.java +++ b/src/main/java/io/oeid/mogakgo/core/properties/JwtProperties.java @@ -1,6 +1,5 @@ package io.oeid.mogakgo.core.properties; -import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,7 +10,7 @@ @Getter @Component @ConfigurationProperties(prefix = "jwt") -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class JwtProperties { private String header; diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerAchievementErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerAchievementErrorExamples.java new file mode 100644 index 00000000..8e7cd030 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerAchievementErrorExamples.java @@ -0,0 +1,9 @@ +package io.oeid.mogakgo.core.properties.swagger.error; + +public class SwaggerAchievementErrorExamples { + + public static final String ACHIEVEMENT_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E130301\",\"message\":\"해당 업적이 존재하지 않습니다.\"}"; + private SwaggerAchievementErrorExamples() { + + } +} diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerChatErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerChatErrorExamples.java index 989a0df2..3dc88a2b 100644 --- a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerChatErrorExamples.java +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerChatErrorExamples.java @@ -3,6 +3,7 @@ public class SwaggerChatErrorExamples { public static final String CHAT_ROOM_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E110301\",\"message\":\"해당 채팅방이 존재하지 않습니다.\"}"; + public static final String CHAT_ROOM_USER_CANNOT_DUPLICATE = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E110102\",\"message\":\"채팅방에 같은 유저가 존재할 수 없습니다.\"}"; private SwaggerChatErrorExamples() { } diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerNotificationErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerNotificationErrorExamples.java new file mode 100644 index 00000000..1002d9ee --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerNotificationErrorExamples.java @@ -0,0 +1,9 @@ +package io.oeid.mogakgo.core.properties.swagger.error; + +public class SwaggerNotificationErrorExamples { + + public static final String NOTIFICATION_FCM_TOKEN_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E060301\",\"message\":\"해당 유저의 FCM 토큰이 존재하지 않습니다.\"}"; + public static final String NOTIFICATION_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":404,\"code\":\"E060302\",\"message\":\"해당 알림이 존재하지 않습니다.\"}"; + private SwaggerNotificationErrorExamples() { + } +} diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerReviewErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerReviewErrorExamples.java new file mode 100644 index 00000000..0d0f7be3 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerReviewErrorExamples.java @@ -0,0 +1,14 @@ +package io.oeid.mogakgo.core.properties.swagger.error; + +public class SwaggerReviewErrorExamples { + + public static final String REVIEW_SENDER_OR_RECEIVER_NOT_FOUND = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E120101\",\"message\":\"리뷰를 작성하기 위한 유저 정보가 존재하지 않습니다.\"}"; + public static final String REVIEW_USER_DUPLICATED = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E120102\",\"message\":\"자신에 대한 리뷰는 작성할 수 없습니다.\"}"; + public static final String REVIEW_PROJECT_NOT_NULL = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E120103\",\"message\":\"리뷰를 작성하기 위한 프로젝트 정보가 존재하지 않습니다.\"}"; + public static final String REVIEW_ALREADY_EXISTS = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E120104\",\"message\":\"이미 작성된 리뷰가 존재합니다.\"}"; + public static final String REVIEW_USER_NOT_MATCH = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E120105\",\"message\":\"리뷰 작성자와 리뷰 대상자가 일치하지 않습니다.\"}"; + public static final String REVIEW_RATING_INVALID = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E120106\",\"message\":\"유효하지 않은 리뷰 평점입니다.\"}"; + + private SwaggerReviewErrorExamples() { + } +} diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserAchievementErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserAchievementErrorExamples.java new file mode 100644 index 00000000..b62a39b0 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserAchievementErrorExamples.java @@ -0,0 +1,12 @@ +package io.oeid.mogakgo.core.properties.swagger.error; + +public class SwaggerUserAchievementErrorExamples { + + public static final String NON_ACHIEVED_USER_ACHIEVEMENT = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E140101\",\"message\":\"대표 업적으로 미달성 업적을 사용할 수 없습니다.\"}"; + public static final String ACHIEVEMENT_SHOULD_BE_DIFFERENT = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E140102\",\"message\":\"해당 업적을 이미 대표 업적으로 사용하고 있습니다.\"}"; + + private SwaggerUserAchievementErrorExamples() { + + } + +} diff --git a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserErrorExamples.java b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserErrorExamples.java index aade99ef..178fff06 100644 --- a/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserErrorExamples.java +++ b/src/main/java/io/oeid/mogakgo/core/properties/swagger/error/SwaggerUserErrorExamples.java @@ -8,6 +8,7 @@ public class SwaggerUserErrorExamples { public static final String USER_AVAILABLE_LIKE_COUNT_IS_ZERO = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E020107\",\"message\":\"당일 사용 가능한 찔러보기 요청을 모두 소진했습니다.\"}"; public static final String USER_AVAILABLE_LIKE_COUNT_IS_FULL = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E020108\",\"message\":\"당일 사용 가능한 찔러보기 최대 요청 횟수를 초과활 수 없습니다.\"}"; public static final String USER_AVATAR_URL_NOT_NULL = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E020109\",\"message\":\"유저 프로필 이미지는 비어있을 수 없습니다.\"}"; + public static final String USER_FORBIDDEN_OPERATION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":403,\"code\":\"E020201\",\"message\":\"사용자의 정보를 변경할 권한이 없습니다.\"}"; private SwaggerUserErrorExamples() { } diff --git a/src/main/java/io/oeid/mogakgo/domain/achievement/domain/entity/UserAchievement.java b/src/main/java/io/oeid/mogakgo/domain/achievement/domain/entity/UserAchievement.java index d0053180..fd1f1603 100644 --- a/src/main/java/io/oeid/mogakgo/domain/achievement/domain/entity/UserAchievement.java +++ b/src/main/java/io/oeid/mogakgo/domain/achievement/domain/entity/UserAchievement.java @@ -1,5 +1,8 @@ package io.oeid.mogakgo.domain.achievement.domain.entity; +import static io.oeid.mogakgo.exception.code.ErrorCode400.NON_ACHIEVED_USER_ACHIEVEMENT; + +import io.oeid.mogakgo.domain.achievement.exception.UserAchievementException; import io.oeid.mogakgo.domain.user.domain.User; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -48,4 +51,10 @@ public class UserAchievement { @Column(name = "created_at") private LocalDateTime createdAt; + public void validateAvailableUpdateAchievement() { + if (this.completed.equals(Boolean.FALSE)) { + throw new UserAchievementException(NON_ACHIEVED_USER_ACHIEVEMENT); + } + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/achievement/exception/AchievementException.java b/src/main/java/io/oeid/mogakgo/domain/achievement/exception/AchievementException.java new file mode 100644 index 00000000..b2fe633c --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/achievement/exception/AchievementException.java @@ -0,0 +1,11 @@ +package io.oeid.mogakgo.domain.achievement.exception; + +import io.oeid.mogakgo.exception.code.ErrorCode; +import io.oeid.mogakgo.exception.exception_class.CustomException; + +public class AchievementException extends CustomException { + + public AchievementException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/achievement/exception/UserAchievementException.java b/src/main/java/io/oeid/mogakgo/domain/achievement/exception/UserAchievementException.java new file mode 100644 index 00000000..6663d80c --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/achievement/exception/UserAchievementException.java @@ -0,0 +1,11 @@ +package io.oeid.mogakgo.domain.achievement.exception; + +import io.oeid.mogakgo.exception.code.ErrorCode; +import io.oeid.mogakgo.exception.exception_class.CustomException; + +public class UserAchievementException extends CustomException { + + public UserAchievementException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/achievement/infrastructure/UserAchievementJpaRepository.java b/src/main/java/io/oeid/mogakgo/domain/achievement/infrastructure/UserAchievementJpaRepository.java new file mode 100644 index 00000000..a9389dc8 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/achievement/infrastructure/UserAchievementJpaRepository.java @@ -0,0 +1,12 @@ +package io.oeid.mogakgo.domain.achievement.infrastructure; + +import io.oeid.mogakgo.domain.achievement.domain.entity.UserAchievement; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface UserAchievementJpaRepository extends JpaRepository { + + @Query("SELECT ua FROM UserAchievement ua WHERE ua.user.id = :userId AND ua.achievement.id = :achievementId") + Optional findByUserAndAchievementId(Long userId, Long achievementId); +} diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatService.java b/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatService.java index d5b4fc9f..5be9013d 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatService.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatService.java @@ -37,7 +37,7 @@ public class ChatService { // 채팅방 리스트 조회 // TODO 마지막 채팅 기록 가져오기 구현 public List findAllChatRoomByUserId(Long userId) { - userCommonService.getUserById(userId); + findUserById(userId); return chatRoomRepository.findAllChatRoomByUserId(userId); } @@ -46,24 +46,35 @@ public List findAllChatRoomByUserId(Long userId) { public ChatRoomCreateRes createChatRoom(Long creatorId, ChatRoomCreateReq request) { Project project = projectRepository.findById(request.getProjectId()) .orElseThrow(() -> new MatchingException(ErrorCode404.PROJECT_NOT_FOUND)); - User creator = userCommonService.getUserById(creatorId); - User sender = userCommonService.getUserById(request.getSenderId()); + User creator = findUserById(creatorId); + User sender = findUserById(request.getSenderId()); ChatRoom chatRoom = chatRoomRepository.save( ChatRoom.builder().project(project).creator(creator).sender(sender).build()); chatRepository.createCollection(chatRoom.getId()); return ChatRoomCreateRes.from(chatRoom); } + @Transactional + public void leaveChatroom(Long userId, String chatRoomId) { + var user = findUserById(userId); + var chatRoom = findChatRoomById(chatRoomId); + + // 채팅방 비활성화 + chatRoom.closeChat(); + + chatRoom.leave(user); + } + // 채팅방 조회 public CursorPaginationResult findAllChatInChatRoom(Long userId, String chatRoomId, CursorPaginationInfoReq pageable) { - var user = userCommonService.getUserById(userId); + var user = findUserById(userId); var chatRoom = findChatRoomById(chatRoomId); chatRoom.validateContainsUser(user); return chatRepository.findAllByCollection(chatRoomId, pageable); } public ChatRoomDataRes findChatRoomDetailData(Long userId, String chatRoomId) { - var user = userCommonService.getUserById(userId); + var user = findUserById(userId); var chatRoom = findChatRoomById(chatRoomId); chatRoom.validateContainsUser(user); var project = projectRepository.findById(chatRoom.getProject().getId()) @@ -75,4 +86,9 @@ private ChatRoom findChatRoomById(String chatRoomId) { return chatRoomRepository.findById(chatRoomId) .orElseThrow(() -> new MatchingException(ErrorCode404.CHAT_ROOM_NOT_FOUND)); } + + private User findUserById(Long userId) { + return userCommonService.getUserById(userId); + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatWebSocketService.java b/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatWebSocketService.java index 7d5570ee..32b365f7 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatWebSocketService.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/application/ChatWebSocketService.java @@ -1,23 +1,21 @@ package io.oeid.mogakgo.domain.chat.application; -import static io.oeid.mogakgo.exception.code.ErrorCode500.CHAT_WEB_SOCKET_ERROR; - +import io.oeid.mogakgo.domain.chat.application.dto.req.ChatReq; +import io.oeid.mogakgo.domain.chat.application.dto.res.ChatDataRes; import io.oeid.mogakgo.domain.chat.entity.ChatRoom; import io.oeid.mogakgo.domain.chat.entity.document.ChatMessage; import io.oeid.mogakgo.domain.chat.entity.enums.ChatStatus; import io.oeid.mogakgo.domain.chat.exception.ChatException; import io.oeid.mogakgo.domain.chat.infrastructure.ChatRepository; import io.oeid.mogakgo.domain.chat.infrastructure.ChatRoomRoomJpaRepository; -import io.oeid.mogakgo.domain.chat.infrastructure.ChatRoomSessionRepository; +import io.oeid.mogakgo.domain.user.application.UserCommonService; +import io.oeid.mogakgo.domain.user.domain.User; import io.oeid.mogakgo.exception.code.ErrorCode400; import io.oeid.mogakgo.exception.code.ErrorCode404; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; @Slf4j @RequiredArgsConstructor @@ -26,43 +24,26 @@ public class ChatWebSocketService { private final ChatRoomRoomJpaRepository chatRoomJpaRepository; private final ChatRepository chatRepository; - private final ChatRoomSessionRepository chatRoomSessionRepository; + private final ChatIdSequenceGeneratorService sequenceGeneratorService; + private final UserCommonService userCommonService; + + public ChatDataRes handleChatMessage(Long userId, String roomId, ChatReq request) { + User user = userCommonService.getUserById(userId); + verifyChatRoomByRoomIdAndUser(roomId, user); + ChatMessage chatMessage = chatRepository.save( + ChatMessage.builder().id(sequenceGeneratorService.generateSequence(roomId)) + .senderId(user.getId()) + .messageType(request.getMessageType()) + .message(request.getMessage()) + .build(), roomId); + return ChatDataRes.from(chatMessage); + } - @Transactional(readOnly = true) - public ChatRoom findChatRoomById(String roomId) { - ChatRoom chatRoom = chatRoomJpaRepository.findById(roomId).orElseThrow(() -> new ChatException(ErrorCode404.CHAT_ROOM_NOT_FOUND)); - if(chatRoom.getStatus().equals(ChatStatus.CLOSED)){ + private void verifyChatRoomByRoomIdAndUser(String roomId, User user) { + ChatRoom chatRoom = chatRoomJpaRepository.findByIdAndUser(roomId, user) + .orElseThrow(() -> new ChatException(ErrorCode404.CHAT_ROOM_NOT_FOUND)); + if (chatRoom.getStatus().equals(ChatStatus.CLOSED)) { throw new ChatException(ErrorCode400.CHAT_ROOM_CLOSED); } - return chatRoom; - } - - public void saveChatMessage(ChatMessage chatMessage, String roomId) { - chatRepository.save(chatMessage, roomId); - } - - public void closeChatRoom(String roomId) { - ChatRoom chatRoom = chatRoomJpaRepository.findById(roomId).orElseThrow(() -> new ChatException(ErrorCode404.CHAT_ROOM_NOT_FOUND)); - chatRoomSessionRepository.removeRoom(chatRoom.getId()); - chatRoom.closeChat(); - } - - public void addSessionToRoom(String roomId, WebSocketSession session) { - chatRoomSessionRepository.addSession(roomId, session); - } - - public void removeSessionFromRoom(String roomId, WebSocketSession session) { - chatRoomSessionRepository.removeSession(roomId, session); - } - - public void sendMessageToEachSocket(String roomId, TextMessage textMessage){ - chatRoomSessionRepository.getSession(roomId).forEach(session -> { - try { - session.sendMessage(textMessage); - } catch (Exception e) { - log.error("sendMessageToEachSocket: {}", e.getMessage()); - throw new ChatException(CHAT_WEB_SOCKET_ERROR); - } - }); } } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/req/ChatReq.java b/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/req/ChatReq.java new file mode 100644 index 00000000..0987f47a --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/req/ChatReq.java @@ -0,0 +1,19 @@ +package io.oeid.mogakgo.domain.chat.application.dto.req; + +import io.oeid.mogakgo.domain.chat.entity.enums.ChatMessageType; +import io.oeid.mogakgo.domain.chat.presentation.dto.ChatApiReq; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ChatReq { + + private ChatMessageType messageType; + private String message; + + public static ChatReq from(ChatApiReq request) { + return new ChatReq(ChatMessageType.valueOf(request.getMessageType()), request.getMessage()); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/res/ChatRoomDataRes.java b/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/res/ChatRoomDataRes.java index 1074fa6f..75edeb37 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/res/ChatRoomDataRes.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/application/dto/res/ChatRoomDataRes.java @@ -18,11 +18,20 @@ public class ChatRoomDataRes { private String meetDetail; @Schema(description = "프로젝트 시작 시간") private LocalDateTime meetStartTime; + @Schema(description = "프로젝트 위치 위도") + private Double meetLocationLatitude; + @Schema(description = "프로젝트 위치 경도") + private Double meetLocationLongitude; @Schema(description = "프로젝트 종료 시간") private LocalDateTime meetEndTime; public static ChatRoomDataRes from(MeetingInfo meetingInfo) { - return new ChatRoomDataRes(meetingInfo.getMeetDetail(), meetingInfo.getMeetStartTime(), + var meetLocation = meetingInfo.getMeetLocation(); + return new ChatRoomDataRes( + meetingInfo.getMeetDetail(), + meetingInfo.getMeetStartTime(), + meetLocation.getX(), + meetLocation.getY(), meetingInfo.getMeetEndTime()); } } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/entity/ChatRoom.java b/src/main/java/io/oeid/mogakgo/domain/chat/entity/ChatRoom.java index 25b1e12d..7cf71557 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/entity/ChatRoom.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/entity/ChatRoom.java @@ -59,7 +59,20 @@ private ChatRoom(Project project, User creator, User sender) { } public void closeChat() { - this.status = ChatStatus.CLOSED; + if (this.status.equals(ChatStatus.OPEN)) { + this.status = ChatStatus.CLOSED; + } + } + + public void leave(User user) { + validateDuplicateUser(); + validateContainsUser(user); + if (isCreator(user)) { + this.creator = null; + } + if (isSender(user)) { + this.sender = null; + } } private void validateUsers(User creator, User sender) { @@ -74,4 +87,24 @@ public void validateContainsUser(User user) { } } + private void validateChatAvailableClosed() { + if (this.status.equals(ChatStatus.CLOSED)) { + throw new ChatException(ErrorCode400.CHAT_ROOM_ALREADY_CLOSED); + } + } + + private boolean isCreator(User user) { + return this.creator.equals(user); + } + + private boolean isSender(User user) { + return this.sender.equals(user); + } + + private void validateDuplicateUser() { + if (this.creator.equals(this.sender)) { + throw new ChatException(ErrorCode400.CHAT_ROOM_USER_CANNOT_DUPLICATE); + } + } + } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/entity/document/ChatMessage.java b/src/main/java/io/oeid/mogakgo/domain/chat/entity/document/ChatMessage.java index 8ab2ca9a..0a9f172c 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/entity/document/ChatMessage.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/entity/document/ChatMessage.java @@ -2,6 +2,7 @@ import io.oeid.mogakgo.domain.chat.entity.enums.ChatMessageType; import java.time.LocalDateTime; +import lombok.Builder; import lombok.Getter; import lombok.Setter; import org.springframework.data.mongodb.core.mapping.Document; @@ -9,18 +10,18 @@ @Getter @Document public class ChatMessage { + @Setter private Long id; private ChatMessageType messageType; - private String chatRoomId; private Long senderId; private String message; private LocalDateTime createdAt; - public ChatMessage(ChatMessageType messageType, String chatRoomId, Long senderId, - String message) { + @Builder + private ChatMessage(Long id, ChatMessageType messageType, Long senderId, String message) { + this.id = id; this.messageType = messageType; - this.chatRoomId = chatRoomId; this.senderId = senderId; this.message = message; this.createdAt = LocalDateTime.now(); diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/handler/CustomWebSocketHandler.java b/src/main/java/io/oeid/mogakgo/domain/chat/handler/CustomWebSocketHandler.java deleted file mode 100644 index 7eb976a6..00000000 --- a/src/main/java/io/oeid/mogakgo/domain/chat/handler/CustomWebSocketHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.oeid.mogakgo.domain.chat.handler; - -import static io.oeid.mogakgo.exception.code.ErrorCode500.CHAT_WEB_SOCKET_ERROR; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.oeid.mogakgo.domain.chat.application.ChatIdSequenceGeneratorService; -import io.oeid.mogakgo.domain.chat.application.ChatWebSocketService; -import io.oeid.mogakgo.domain.chat.entity.ChatRoom; -import io.oeid.mogakgo.domain.chat.entity.document.ChatMessage; -import io.oeid.mogakgo.domain.chat.exception.ChatException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.WebSocketMessage; -import org.springframework.web.socket.WebSocketSession; - -@Slf4j -@RequiredArgsConstructor -@Component -public class CustomWebSocketHandler implements WebSocketHandler { - - private final ObjectMapper objectMapper; - private final ChatWebSocketService chatWebSocketService; - private final ChatIdSequenceGeneratorService sequenceGeneratorService; - - @Override - public void afterConnectionEstablished(WebSocketSession session) throws Exception { - log.info("afterConnectionEstablished: {}", session.getId()); - } - - @Override - public void handleMessage(WebSocketSession session, WebSocketMessage message) - throws Exception { - TextMessage textMessage = (TextMessage) message; - String payload = textMessage.getPayload(); - ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class); - ChatRoom chatRoom = chatWebSocketService.findChatRoomById(chatMessage.getChatRoomId()); - switch (chatMessage.getMessageType()) { - case ENTER -> chatWebSocketService.addSessionToRoom(chatRoom.getId(), session); - case QUIT -> { - chatWebSocketService.removeSessionFromRoom(chatRoom.getId(), session); - chatWebSocketService.closeChatRoom(chatRoom.getId()); - } - default -> chatWebSocketService.sendMessageToEachSocket(chatRoom.getId(), textMessage); - } - chatMessage.setId(sequenceGeneratorService.generateSequence(chatMessage.getChatRoomId())); - chatWebSocketService.saveChatMessage(chatMessage, chatRoom.getId()); - } - - @Override - public void handleTransportError(WebSocketSession session, Throwable exception) - throws Exception { - log.warn("handleTransportError: {}", exception.getMessage()); - throw new ChatException(CHAT_WEB_SOCKET_ERROR); - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) - throws Exception { - log.info("afterConnectionClosed: {}, {}", session.getId(), closeStatus); - } - - @Override - public boolean supportsPartialMessages() { - return false; - } -} diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomCustomRepositoryImpl.java b/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomCustomRepositoryImpl.java index c6ebd618..9edc42df 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomCustomRepositoryImpl.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomCustomRepositoryImpl.java @@ -46,7 +46,7 @@ public List findAllChatRoomByUserId(Long userId) { ) ) ) - ).from(chatRoom).join(chatRoom.creator, creator).join(chatRoom.sender, sender) + ).from(chatRoom).leftJoin(chatRoom.creator, creator).leftJoin(chatRoom.sender, sender) .where(chatRoom.creator.id.eq(userId).or(chatRoom.sender.id.eq(userId))) .fetch(); } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomRoomJpaRepository.java b/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomRoomJpaRepository.java index 4ed03099..4cc1f3f7 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomRoomJpaRepository.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomRoomJpaRepository.java @@ -1,7 +1,8 @@ package io.oeid.mogakgo.domain.chat.infrastructure; import io.oeid.mogakgo.domain.chat.entity.ChatRoom; -import java.util.List; +import io.oeid.mogakgo.domain.user.domain.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -10,6 +11,6 @@ public interface ChatRoomRoomJpaRepository extends JpaRepository, ChatRoomCustomRepository { - @Query("select c from ChatRoom c where c.creator.id = ?1 or c.sender.id = ?1 order by c.id DESC") - List findAllByUserId(Long id); + @Query("select c from ChatRoom c where c.id = ?1 and (c.creator = ?2 or c.sender = ?2)") + Optional findByIdAndUser(String id, User user); } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomSessionRepository.java b/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomSessionRepository.java deleted file mode 100644 index 43f95e2f..00000000 --- a/src/main/java/io/oeid/mogakgo/domain/chat/infrastructure/ChatRoomSessionRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.oeid.mogakgo.domain.chat.infrastructure; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.springframework.stereotype.Repository; -import org.springframework.web.socket.WebSocketSession; - -@Repository -public class ChatRoomSessionRepository { - private final Map> sessionStorage = new ConcurrentHashMap<>(); - - public void addSession(String roomId, WebSocketSession session) { - sessionStorage.computeIfAbsent(roomId, key -> ConcurrentHashMap.newKeySet()).add(session); - } - - public Set getSession(String roomId) { - return sessionStorage.get(roomId); - } - - public void removeSession(String roomId, WebSocketSession session) { - sessionStorage.get(roomId).remove(session); - } - - public void removeRoom(String roomId) { - sessionStorage.remove(roomId); - } -} diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/interceptor/ChatInterceptor.java b/src/main/java/io/oeid/mogakgo/domain/chat/interceptor/ChatInterceptor.java new file mode 100644 index 00000000..60e97547 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/chat/interceptor/ChatInterceptor.java @@ -0,0 +1,47 @@ +package io.oeid.mogakgo.domain.chat.interceptor; + +import io.oeid.mogakgo.domain.auth.jwt.JwtHelper; +import io.oeid.mogakgo.domain.chat.exception.ChatException; +import io.oeid.mogakgo.exception.code.ErrorCode401; +import java.time.LocalDateTime; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ChatInterceptor implements ChannelInterceptor { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private final JwtHelper jwtHelper; + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + if (Objects.requireNonNull(accessor.getCommand()).equals(StompCommand.CONNECT)) { + log.info("CONNECT REQUEST: {} at: {}", accessor.getSessionId(), LocalDateTime.now()); + verifyAccessToken(accessor); + } + return message; + } + + private void verifyAccessToken(StompHeaderAccessor stompHeaderAccessor) { + var values = stompHeaderAccessor.getNativeHeader(AUTHORIZATION_HEADER); + if (values == null || values.isEmpty()) { + throw new ChatException(ErrorCode401.AUTH_MISSING_CREDENTIALS); + } + String accessToken = values.get(0); + if (accessToken == null || accessToken.isBlank()) { + throw new ChatException(ErrorCode401.AUTH_MISSING_CREDENTIALS); + } + accessToken = accessToken.substring(7); + jwtHelper.verify(accessToken); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatController.java b/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatController.java index e916f352..27631f4a 100644 --- a/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatController.java +++ b/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatController.java @@ -13,13 +13,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -// TODO: FIX SWAGGER @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/chat") @@ -32,7 +32,7 @@ public ResponseEntity> getChatRoomList(@UserId Long user return ResponseEntity.ok(chatService.findAllChatRoomByUserId(userId)); } - @GetMapping("detail/{chatRoomId}") + @GetMapping("/detail/{chatRoomId}") public ResponseEntity getChatRoomDetailData(@UserId Long userId, @PathVariable String chatRoomId) { return ResponseEntity.ok(chatService.findChatRoomDetailData(userId, chatRoomId)); @@ -44,4 +44,12 @@ public ResponseEntity> getChatData( @UserId Long userId, @Valid @ModelAttribute CursorPaginationInfoReq pageable) { return ResponseEntity.ok(chatService.findAllChatInChatRoom(userId, chatRoomId, pageable)); } + + @PatchMapping("/{chatRoomId}") + public ResponseEntity leaveChatRoom( + @UserId Long userId, @PathVariable String chatRoomId + ) { + chatService.leaveChatroom(userId, chatRoomId); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatWebSocketController.java b/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatWebSocketController.java new file mode 100644 index 00000000..caccc839 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/chat/presentation/ChatWebSocketController.java @@ -0,0 +1,27 @@ +package io.oeid.mogakgo.domain.chat.presentation; + +import io.oeid.mogakgo.domain.chat.application.ChatWebSocketService; +import io.oeid.mogakgo.domain.chat.application.dto.req.ChatReq; +import io.oeid.mogakgo.domain.chat.application.dto.res.ChatDataRes; +import io.oeid.mogakgo.domain.chat.presentation.dto.ChatApiReq; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/chat") +@RequiredArgsConstructor +@Slf4j +public class ChatWebSocketController { + + private final ChatWebSocketService chatWebSocketService; + @MessageMapping("/chatroom/{chatRoomId}") + @SendTo("/topic/chatroom/{chatRoomId}") + public ChatDataRes sendChatData(@DestinationVariable("chatRoomId") String chatRoomId, ChatApiReq request) { + return chatWebSocketService.handleChatMessage(request.getUserId(), chatRoomId, ChatReq.from(request)); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/chat/presentation/dto/ChatApiReq.java b/src/main/java/io/oeid/mogakgo/domain/chat/presentation/dto/ChatApiReq.java new file mode 100644 index 00000000..9cf6c87c --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/chat/presentation/dto/ChatApiReq.java @@ -0,0 +1,20 @@ +package io.oeid.mogakgo.domain.chat.presentation.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "채팅 API 요청 DTO") +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class ChatApiReq { + + @Schema(description = "유저 ID", example = "2") + private Long userId; + @Schema(description = "메시지 타입", example = "TALK") + private String messageType; + @Schema(description = "메시지", example = "안녕하세요", nullable = true) + private String message; +} diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java b/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java index 6a863886..3c074d36 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/application/NotificationService.java @@ -3,12 +3,15 @@ import io.oeid.mogakgo.common.base.CursorPaginationInfoReq; import io.oeid.mogakgo.common.base.CursorPaginationResult; import io.oeid.mogakgo.domain.notification.application.dto.req.NotificationCreateRequest; +import io.oeid.mogakgo.domain.notification.application.dto.res.NotificationCheckedRes; import io.oeid.mogakgo.domain.notification.application.dto.res.NotificationCreateResponse; import io.oeid.mogakgo.domain.notification.domain.Notification; +import io.oeid.mogakgo.domain.notification.exception.NotificationException; import io.oeid.mogakgo.domain.notification.infrastructure.NotificationJpaRepository; import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; import io.oeid.mogakgo.domain.user.application.UserCommonService; import io.oeid.mogakgo.domain.user.domain.User; +import io.oeid.mogakgo.exception.code.ErrorCode404; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -37,4 +40,13 @@ public CursorPaginationResult getNotifications(Long us User user = userCommonService.getUserById(userId); return notificationRepository.findByUserIdWithPagination(user.getId(), pageable); } + + @Transactional + public NotificationCheckedRes checkedNotification(Long userId, Long notificationId) { + User user = userCommonService.getUserById(userId); + Notification notification = notificationRepository.findByIdAndReceiver(notificationId, user) + .orElseThrow(() -> new NotificationException(ErrorCode404.NOTIFICATION_NOT_FOUND)); + notification.markAsChecked(); + return new NotificationCheckedRes(notification.getId()); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCheckedRes.java b/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCheckedRes.java new file mode 100644 index 00000000..1cce8040 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/notification/application/dto/res/NotificationCheckedRes.java @@ -0,0 +1,16 @@ +package io.oeid.mogakgo.domain.notification.application.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "알림 확인 응답") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class NotificationCheckedRes { + + @Schema(description = "알림 ID", example = "1", implementation = Long.class) + private Long notificationId; +} diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java b/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java index 10488682..89e2604d 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/domain/Notification.java @@ -81,4 +81,8 @@ private String validateDetailData(String detailData) { } return detailData; } + + public void markAsChecked() { + this.checkedYn = true; + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java index 261771f8..df5091fc 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/infrastructure/NotificationJpaRepository.java @@ -1,6 +1,8 @@ package io.oeid.mogakgo.domain.notification.infrastructure; import io.oeid.mogakgo.domain.notification.domain.Notification; +import io.oeid.mogakgo.domain.user.domain.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,4 +10,5 @@ public interface NotificationJpaRepository extends JpaRepository, NotificationCustomRepository { + Optional findByIdAndReceiver(Long id, User receiver); } \ No newline at end of file diff --git a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java index 49fac334..0546a2d0 100644 --- a/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java +++ b/src/main/java/io/oeid/mogakgo/domain/notification/presentation/NotificationController.java @@ -6,6 +6,7 @@ import io.oeid.mogakgo.common.swagger.template.NotificationSwagger; import io.oeid.mogakgo.domain.notification.application.FCMNotificationService; import io.oeid.mogakgo.domain.notification.application.NotificationService; +import io.oeid.mogakgo.domain.notification.application.dto.res.NotificationCheckedRes; import io.oeid.mogakgo.domain.notification.presentation.dto.req.FCMTokenApiRequest; import io.oeid.mogakgo.domain.notification.presentation.dto.res.NotificationPublicApiRes; import jakarta.validation.Valid; @@ -13,6 +14,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +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; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,4 +41,10 @@ public ResponseEntity> getByUse @UserId Long id, @Valid @ModelAttribute CursorPaginationInfoReq pageable) { return ResponseEntity.ok().body(notificationService.getNotifications(id, pageable)); } + + @PatchMapping("/{notificationId}") + public ResponseEntity markCheckedNotification(@UserId Long userId, + @PathVariable Long notificationId) { + return ResponseEntity.ok(notificationService.checkedNotification(userId, notificationId)); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java b/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java index 118576c9..e14ce5ce 100644 --- a/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java +++ b/src/main/java/io/oeid/mogakgo/domain/profile/application/ProfileCardService.java @@ -44,7 +44,7 @@ public CursorPaginationResult getRandomOrderedProfileCard null, region, pageable ); - Collections.shuffle(profiles.getData()); + Collections.shuffle(profiles.getData().subList(0, profiles.getData().size() - 1)); return profiles; } diff --git a/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java b/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java index 3c4dbe7c..0cab5da4 100644 --- a/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java +++ b/src/main/java/io/oeid/mogakgo/domain/project/application/ProjectService.java @@ -147,7 +147,7 @@ public CursorPaginationResult getRandomOrderedProjectsByReg ); // 요청할 때마다 랜덤 정렬 - Collections.shuffle(projects.getData()); + Collections.shuffle(projects.getData().subList(0, projects.getData().size() - 1)); return projects; } diff --git a/src/main/java/io/oeid/mogakgo/domain/review/application/ReviewService.java b/src/main/java/io/oeid/mogakgo/domain/review/application/ReviewService.java index 48e1ab90..c6af3e0b 100644 --- a/src/main/java/io/oeid/mogakgo/domain/review/application/ReviewService.java +++ b/src/main/java/io/oeid/mogakgo/domain/review/application/ReviewService.java @@ -24,7 +24,8 @@ public class ReviewService { private final UserCommonService userCommonService; @Transactional - public ReviewCreateRes createNewReview(ReviewCreateReq request) { + public ReviewCreateRes createNewReview(Long userId, ReviewCreateReq request) { + validateUser(userId, request.getSenderId()); reviewRepository.findReviewByProjectData(request.getSenderId(), request.getReceiverId(), request.getProjectId()).ifPresent(review -> { throw new ReviewException(ErrorCode400.REVIEW_ALREADY_EXISTS); @@ -53,4 +54,9 @@ private double calculateProjectTime(LocalDateTime meetStartTime, LocalDateTime m return hours + minutes / 60; } + private void validateUser(Long userId, Long senderId) { + if (!userId.equals(senderId)) { + throw new ReviewException(ErrorCode400.REVIEW_USER_NOT_MATCH); + } + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/review/application/dto/req/ReviewCreateReq.java b/src/main/java/io/oeid/mogakgo/domain/review/application/dto/req/ReviewCreateReq.java index 2aa0ba1f..b25f283f 100644 --- a/src/main/java/io/oeid/mogakgo/domain/review/application/dto/req/ReviewCreateReq.java +++ b/src/main/java/io/oeid/mogakgo/domain/review/application/dto/req/ReviewCreateReq.java @@ -1,14 +1,23 @@ package io.oeid.mogakgo.domain.review.application.dto.req; import io.oeid.mogakgo.domain.review.domain.enums.ReviewRating; +import io.oeid.mogakgo.domain.review.presentation.dto.req.ReviewCreateApiReq; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @Getter -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class ReviewCreateReq { private Long senderId; private Long receiverId; private Long projectId; private ReviewRating rating; + + public static ReviewCreateReq from(ReviewCreateApiReq request) { + return new ReviewCreateReq(request.getSenderId(), + request.getReceiverId(), + request.getProjectId(), + ReviewRating.from(request.getRating())); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/review/application/dto/res/ReviewCreateRes.java b/src/main/java/io/oeid/mogakgo/domain/review/application/dto/res/ReviewCreateRes.java index 6f14a1e9..5c60a42b 100644 --- a/src/main/java/io/oeid/mogakgo/domain/review/application/dto/res/ReviewCreateRes.java +++ b/src/main/java/io/oeid/mogakgo/domain/review/application/dto/res/ReviewCreateRes.java @@ -2,6 +2,7 @@ import io.oeid.mogakgo.domain.review.domain.Review; import io.oeid.mogakgo.domain.review.domain.enums.ReviewRating; +import io.oeid.mogakgo.domain.review.presentation.dto.res.ReviewCreateApiRes; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -27,4 +28,15 @@ public static ReviewCreateRes from(Review review) { review.getCreatedAt() ); } + + public ReviewCreateApiRes toApiResponse(){ + return new ReviewCreateApiRes( + id, + senderId, + receiverId, + projectId, + rating.getValue(), + createdAt.toString() + ); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/review/domain/enums/ReviewRating.java b/src/main/java/io/oeid/mogakgo/domain/review/domain/enums/ReviewRating.java index 29deeb42..c0fba762 100644 --- a/src/main/java/io/oeid/mogakgo/domain/review/domain/enums/ReviewRating.java +++ b/src/main/java/io/oeid/mogakgo/domain/review/domain/enums/ReviewRating.java @@ -1,5 +1,7 @@ package io.oeid.mogakgo.domain.review.domain.enums; +import io.oeid.mogakgo.domain.review.exception.ReviewException; +import io.oeid.mogakgo.exception.code.ErrorCode400; import lombok.Getter; @Getter @@ -15,4 +17,13 @@ public enum ReviewRating { ReviewRating(int value) { this.value = value; } + + public static ReviewRating from(int rating){ + for(ReviewRating reviewRating : ReviewRating.values()){ + if(reviewRating.value == rating){ + return reviewRating; + } + } + throw new ReviewException(ErrorCode400.REVIEW_RATING_INVALID); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/review/presentation/ReviewController.java b/src/main/java/io/oeid/mogakgo/domain/review/presentation/ReviewController.java new file mode 100644 index 00000000..27d61951 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/review/presentation/ReviewController.java @@ -0,0 +1,31 @@ +package io.oeid.mogakgo.domain.review.presentation; + +import io.oeid.mogakgo.common.annotation.UserId; +import io.oeid.mogakgo.common.swagger.template.ReviewSwagger; +import io.oeid.mogakgo.domain.review.application.ReviewService; +import io.oeid.mogakgo.domain.review.application.dto.req.ReviewCreateReq; +import io.oeid.mogakgo.domain.review.presentation.dto.req.ReviewCreateApiReq; +import io.oeid.mogakgo.domain.review.presentation.dto.res.ReviewCreateApiRes; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/review") +@RequiredArgsConstructor +public class ReviewController implements ReviewSwagger { + + private final ReviewService reviewService; + + @PostMapping + public ResponseEntity createReviewApi(@UserId Long userId, + @Valid @RequestBody ReviewCreateApiReq request) { + var result = reviewService.createNewReview(userId, ReviewCreateReq.from(request)); + return ResponseEntity.ok(result.toApiResponse()); + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/review/presentation/dto/req/ReviewCreateApiReq.java b/src/main/java/io/oeid/mogakgo/domain/review/presentation/dto/req/ReviewCreateApiReq.java new file mode 100644 index 00000000..40e06c2a --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/review/presentation/dto/req/ReviewCreateApiReq.java @@ -0,0 +1,31 @@ +package io.oeid.mogakgo.domain.review.presentation.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +@Schema(description = "리뷰 생성 요청") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ReviewCreateApiReq { + + @NotNull(message = "리뷰 생성자 ID는 필수입니다.") + @Schema(description = "리뷰 생성자 ID", example = "1") + private Long senderId; + + @NotNull(message = "리뷰 대상자 ID는 필수입니다.") + @Schema(description = "리뷰 대상자 ID", example = "2") + private Long receiverId; + + @NotNull(message = "프로젝트 ID는 필수입니다.") + @Schema(description = "프로젝트 ID", example = "3") + private Long projectId; + + @Range(min = 1, max = 5, message = "평점은 1~5 사이의 값이어야 합니다.") + @Schema(description = "평점", example = "5", minimum = "1", maximum = "5") + private Integer rating; +} diff --git a/src/main/java/io/oeid/mogakgo/domain/review/presentation/dto/res/ReviewCreateApiRes.java b/src/main/java/io/oeid/mogakgo/domain/review/presentation/dto/res/ReviewCreateApiRes.java new file mode 100644 index 00000000..06e92704 --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/review/presentation/dto/res/ReviewCreateApiRes.java @@ -0,0 +1,31 @@ +package io.oeid.mogakgo.domain.review.presentation.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "리뷰 생성 API 응답") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ReviewCreateApiRes { + + @Schema(description = "리뷰 ID", example = "1") + private Long id; + + @Schema(description = "리뷰 작성자 ID", example = "1") + private Long senderId; + + @Schema(description = "리뷰 대상자 ID", example = "2") + private Long receiverId; + + @Schema(description = "프로젝트 ID", example = "1") + private Long projectId; + + @Schema(description = "프로젝트 ID", example = "1") + private Integer rating; + + @Schema(description = "리뷰 생성일시", example = "2021-08-01T00:00:00") + private String createdAt; +} diff --git a/src/main/java/io/oeid/mogakgo/domain/user/application/UserService.java b/src/main/java/io/oeid/mogakgo/domain/user/application/UserService.java index fc4d493a..2bfe55f3 100644 --- a/src/main/java/io/oeid/mogakgo/domain/user/application/UserService.java +++ b/src/main/java/io/oeid/mogakgo/domain/user/application/UserService.java @@ -1,7 +1,14 @@ package io.oeid.mogakgo.domain.user.application; +import static io.oeid.mogakgo.exception.code.ErrorCode400.NON_ACHIEVED_USER_ACHIEVEMENT; +import static io.oeid.mogakgo.exception.code.ErrorCode404.ACHIEVEMENT_NOT_FOUND; + import io.oeid.mogakgo.domain.achievement.domain.entity.Achievement; +import io.oeid.mogakgo.domain.achievement.domain.entity.UserAchievement; +import io.oeid.mogakgo.domain.achievement.exception.AchievementException; +import io.oeid.mogakgo.domain.achievement.exception.UserAchievementException; import io.oeid.mogakgo.domain.achievement.infrastructure.AchievementJpaRepository; +import io.oeid.mogakgo.domain.achievement.infrastructure.UserAchievementJpaRepository; import io.oeid.mogakgo.domain.profile.application.ProfileCardService; import io.oeid.mogakgo.domain.profile.application.dto.req.UserProfileCardReq; import io.oeid.mogakgo.domain.user.application.dto.req.UserSignUpRequest; @@ -19,6 +26,7 @@ import io.oeid.mogakgo.domain.user.exception.UserException; import io.oeid.mogakgo.domain.user.infrastructure.UserDevelopLanguageTagJpaRepository; import io.oeid.mogakgo.domain.user.infrastructure.UserWantedJobTagJpaRepository; +import io.oeid.mogakgo.domain.user.presentation.dto.req.UserAchievementUpdateApiRequest; import io.oeid.mogakgo.domain.user.util.UserGithubUtil; import io.oeid.mogakgo.exception.code.ErrorCode400; import java.util.ArrayList; @@ -40,6 +48,7 @@ public class UserService { private final UserDevelopLanguageTagJpaRepository userDevelopLanguageTagRepository; private final UserGithubUtil userGithubUtil; private final AchievementJpaRepository achievementRepository; + private final UserAchievementJpaRepository userAchievementRepository; @Transactional public UserSignUpResponse userSignUp(UserSignUpRequest userSignUpRequest) { @@ -86,10 +95,7 @@ public UserProfileResponse getUserProfile(Long userId) { @Transactional public UserUpdateRes updateUserInfos(Long userId, UserUpdateReq request) { User user = userCommonService.getUserById(userId); - Achievement achievement = achievementRepository.findById(request.getAchievementId()) - .orElseThrow(); - user.updateUserInfos(request.getUsername(), request.getAvatarUrl(), request.getBio(), - achievement); + user.updateUserInfos(request.getUsername(), request.getAvatarUrl(), request.getBio()); validateWantedJobDuplicate(request.getWantedJobs()); for (WantedJob wantedJob : request.getWantedJobs()) { userWantedJobTagRepository.save(UserWantedJobTag.builder() @@ -97,10 +103,30 @@ public UserUpdateRes updateUserInfos(Long userId, UserUpdateReq request) { .wantedJob(wantedJob) .build()); } - profileCardService.create(UserProfileCardReq.of(user)); return UserUpdateRes.from(user); } + @Transactional + public Long updateAchievement(Long userId, UserAchievementUpdateApiRequest request) { + User user = userCommonService.getUserById(userId); + + // 토큰 값과 업데이트하려는 사용자 ID의 일치 여부 검증 + user.validateUpdater(request.getUserId()); + + // 해당 업적의 존재 여부 검증 + validateAchievement(request.getAchievementId()); + + UserAchievement userAchievement = userAchievementRepository + .findByUserAndAchievementId(userId, request.getAchievementId()) + .orElseThrow(() -> new UserAchievementException(NON_ACHIEVED_USER_ACHIEVEMENT)); + + // 변경하려는 업적의 달성 여부 검증 + userAchievement.validateAvailableUpdateAchievement(); + + user.updateAchievement(userAchievement.getAchievement()); + return user.getId(); + } + @Transactional public void deleteUser(Long userId) { User user = userCommonService.getUserById(userId); @@ -119,5 +145,8 @@ private void validateWantedJobDuplicate(List wantedJobs) { } } - + private Achievement validateAchievement(Long achievementId) { + return achievementRepository.findById(achievementId) + .orElseThrow(() -> new AchievementException(ACHIEVEMENT_NOT_FOUND)); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/user/application/dto/res/UserUpdateRes.java b/src/main/java/io/oeid/mogakgo/domain/user/application/dto/res/UserUpdateRes.java index da4e11df..f2987be5 100644 --- a/src/main/java/io/oeid/mogakgo/domain/user/application/dto/res/UserUpdateRes.java +++ b/src/main/java/io/oeid/mogakgo/domain/user/application/dto/res/UserUpdateRes.java @@ -15,15 +15,13 @@ public class UserUpdateRes { private String bio; private String avatarUrl; private List wantedJobs; - private Long achievementId; public static UserUpdateRes from(User user){ return new UserUpdateRes( user.getUsername(), user.getBio(), user.getAvatarUrl(), - user.getUserWantedJobTags().stream().map(UserWantedJobTag::getWantedJob).toList(), - user.getAchievement().getId() + user.getUserWantedJobTags().stream().map(UserWantedJobTag::getWantedJob).toList() ); } } diff --git a/src/main/java/io/oeid/mogakgo/domain/user/domain/User.java b/src/main/java/io/oeid/mogakgo/domain/user/domain/User.java index 2ed682d9..bab5a084 100644 --- a/src/main/java/io/oeid/mogakgo/domain/user/domain/User.java +++ b/src/main/java/io/oeid/mogakgo/domain/user/domain/User.java @@ -1,9 +1,12 @@ package io.oeid.mogakgo.domain.user.domain; +import static io.oeid.mogakgo.exception.code.ErrorCode400.ACHIEVEMENT_SHOULD_BE_DIFFERENT; import static io.oeid.mogakgo.exception.code.ErrorCode400.USER_AVAILABLE_LIKE_AMOUNT_IS_FULL; import static io.oeid.mogakgo.exception.code.ErrorCode400.USER_AVAILABLE_LIKE_COUNT_IS_ZERO; +import static io.oeid.mogakgo.exception.code.ErrorCode403.USER_FORBIDDEN_OPERATION; import io.oeid.mogakgo.domain.achievement.domain.entity.Achievement; +import io.oeid.mogakgo.domain.achievement.exception.UserAchievementException; import io.oeid.mogakgo.domain.geo.domain.enums.Region; import io.oeid.mogakgo.domain.review.domain.enums.ReviewRating; import io.oeid.mogakgo.domain.user.domain.enums.Role; @@ -205,15 +208,20 @@ public void updateRegion(Region region) { } } - public void updateUserInfos(String username, String avatarUrl, String bio, - Achievement achievement) { + public void updateUserInfos(String username, String avatarUrl, String bio) { updateUsername(username); this.avatarUrl = verifyAvatarUrl(avatarUrl); this.bio = bio; - this.achievement = achievement; deleteAllWantJobTags(); } + public void updateAchievement(Achievement achievement) { + if (this.achievement != null && this.achievement.equals(achievement)) { + throw new UserAchievementException(ACHIEVEMENT_SHOULD_BE_DIFFERENT); + } + this.achievement = achievement; + } + public void updateJandiRateByReview(ReviewRating rating, double time) { this.jandiRate += rating.getValue() * time * JANDI_WEIGHT; } @@ -233,5 +241,10 @@ private String verifyAvatarUrl(String avatarUrl) { return avatarUrl; } + public void validateUpdater(Long userId) { + if (!this.id.equals(userId)) { + throw new UserException(USER_FORBIDDEN_OPERATION); + } + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/user/presentation/UserController.java b/src/main/java/io/oeid/mogakgo/domain/user/presentation/UserController.java index 0f03059e..fd370d88 100644 --- a/src/main/java/io/oeid/mogakgo/domain/user/presentation/UserController.java +++ b/src/main/java/io/oeid/mogakgo/domain/user/presentation/UserController.java @@ -6,8 +6,10 @@ import io.oeid.mogakgo.domain.user.application.UserService; import io.oeid.mogakgo.domain.user.application.dto.req.UserUpdateReq; import io.oeid.mogakgo.domain.user.application.dto.res.UserJandiRateRes; +import io.oeid.mogakgo.domain.user.presentation.dto.req.UserAchievementUpdateApiRequest; import io.oeid.mogakgo.domain.user.presentation.dto.req.UserSignUpApiReq; import io.oeid.mogakgo.domain.user.presentation.dto.req.UserUpdateApiReq; +import io.oeid.mogakgo.domain.user.presentation.dto.res.UserAchievementUpdateApiResponse; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserDevelopLanguageApiRes; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserMatchingStatus; import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse; @@ -77,4 +79,12 @@ public ResponseEntity userMatchingStatusApi(@UserId Long use return ResponseEntity.ok( new UserMatchingStatus(userMatchingService.hasProgressMatching(userId))); } + + @PatchMapping("/achievement") + public ResponseEntity updateUserMainAchievement( + @UserId Long userId, @Valid @RequestBody UserAchievementUpdateApiRequest request + ) { + Long id = userService.updateAchievement(userId, request); + return ResponseEntity.ok().body(UserAchievementUpdateApiResponse.from(id)); + } } diff --git a/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/req/UserAchievementUpdateApiRequest.java b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/req/UserAchievementUpdateApiRequest.java new file mode 100644 index 00000000..1ad15f6f --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/req/UserAchievementUpdateApiRequest.java @@ -0,0 +1,26 @@ +package io.oeid.mogakgo.domain.user.presentation.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(description = "사용자의 대표 업적 수정 요청 DTO") +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserAchievementUpdateApiRequest { + + @Schema(description = "대표 업적을 수정하려는 사용자 ID", example = "11", implementation = Long.class) + @NotNull + private final Long userId; + + @Schema(description = "수정하려는 대표 업적 ID", example = "2", implementation = Long.class) + @NotNull + private final Long achievementId; + + public static UserAchievementUpdateApiRequest of(Long userId, Long achievementId) { + return new UserAchievementUpdateApiRequest(userId, achievementId); + } + +} diff --git a/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserAchievementUpdateApiResponse.java b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserAchievementUpdateApiResponse.java new file mode 100644 index 00000000..76060dde --- /dev/null +++ b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserAchievementUpdateApiResponse.java @@ -0,0 +1,21 @@ +package io.oeid.mogakgo.domain.user.presentation.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Schema(description = "사용자의 대표 업적 수정 요청의 응답 DTO") +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserAchievementUpdateApiResponse { + + @Schema(description = "대표 업적을 수정한 사용자 ID", example = "11", implementation = Long.class) + @NotNull + private final Long userId; + + public static UserAchievementUpdateApiResponse from(Long userId) { + return new UserAchievementUpdateApiResponse(userId); + } +} diff --git a/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserUpdateApiRes.java b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserUpdateApiRes.java index 08309bbf..6822d19e 100644 --- a/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserUpdateApiRes.java +++ b/src/main/java/io/oeid/mogakgo/domain/user/presentation/dto/res/UserUpdateApiRes.java @@ -23,16 +23,13 @@ public class UserUpdateApiRes { private String avatarUrl; @Schema(description = "유저가 원하는 직군", example = "[\"BACKEND\", \"FRONTEND\"]") private List wantedJobs; - @Schema(description = "유저의 업적 ID", example = "1") - private Long achievementId; public static UserUpdateApiRes from(UserUpdateRes userUpdateRes) { return new UserUpdateApiRes( userUpdateRes.getUsername(), userUpdateRes.getBio(), userUpdateRes.getAvatarUrl(), - userUpdateRes.getWantedJobs().stream().map(WantedJob::getJobName).toList(), - userUpdateRes.getAchievementId() + userUpdateRes.getWantedJobs().stream().map(WantedJob::getJobName).toList() ); } } diff --git a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java index 84fa77a6..c3b48cde 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode400.java @@ -56,11 +56,17 @@ public enum ErrorCode400 implements ErrorCode { CHAT_ROOM_CLOSED("E110101", "채팅방이 종료되어 채팅을 할 수 없습니다."), CHAT_ROOM_USER_CANNOT_DUPLICATE("E110102", "채팅방에 중복된 유저가 있습니다."), CHAT_ROOM_USER_NOT_CONTAINS("E110103", "채팅방에 해당 유저가 없습니다."), + CHAT_ROOM_ALREADY_CLOSED("E110104", "채팅방이 이미 종료되었습니다."), REVIEW_SENDER_OR_RECEIVER_NOT_FOUND("E120101", "리뷰를 작성하기 위한 유저 정보가 존재하지 않습니다."), REVIEW_USER_DUPLICATED("E120102", "자신에 대한 리뷰는 작성할 수 없습니다."), REVIEW_PROJECT_NOT_NULL("E120103", "리뷰를 작성하기 위한 프로젝트 정보가 존재하지 않습니다."), REVIEW_ALREADY_EXISTS("E120104", "이미 작성된 리뷰가 존재합니다."), + REVIEW_USER_NOT_MATCH("E120105", "리뷰 작성자와 리뷰 대상자가 일치하지 않습니다."), + REVIEW_RATING_INVALID("E120106", "유효하지 않은 리뷰 평점입니다."), + + NON_ACHIEVED_USER_ACHIEVEMENT("E140101", "미달성 업적을 사용할 수 없습니다."), + ACHIEVEMENT_SHOULD_BE_DIFFERENT("E140102", "이미 해당 업적을 대표 업적으로 사용중입니다."), ; private final HttpStatus httpStatus = HttpStatus.BAD_REQUEST; diff --git a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java index 4206ca00..4487ad6f 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode403.java @@ -11,6 +11,7 @@ public enum ErrorCode403 implements ErrorCode { INVALID_CERT_INFORMATION("E070201", "동네 인증을 수행할 권한이 없습니다."), PROFILE_CARD_LIKE_FORBIDDEN_OPERATION("E040101", "본인 프로필 카드의 좋아요 수만 조회할 수 있습니다."), MATCHING_FORBIDDEN_OPERATION("E090201", "해당 매칭에 대한 권한이 없습니다."), + USER_FORBIDDEN_OPERATION("E020201", "사용자 정보를 수정할 권한이 없습니다."), ; private final HttpStatus httpStatus = HttpStatus.FORBIDDEN; diff --git a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java index 2f397ec3..39e17669 100644 --- a/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java +++ b/src/main/java/io/oeid/mogakgo/exception/code/ErrorCode404.java @@ -9,9 +9,11 @@ public enum ErrorCode404 implements ErrorCode { PROJECT_NOT_FOUND("E030301", "해당 프로젝트가 존재하지 않습니다."), PROFILE_CARD_NOT_FOUND("E040301", "해당 프로필 카드가 존재하지 않습니다."), NOTIFICATION_FCM_TOKEN_NOT_FOUND("E060301", "해당 유저의 FCM 토큰이 존재하지 않습니다."), + NOTIFICATION_NOT_FOUND("E060302", "해당 알림이 존재하지 않습니다."), PROJECT_JOIN_REQUEST_NOT_FOUND("E050301", "해당 프로젝트 참여 요청이 존재하지 않습니다."), MATCHING_NOT_FOUND("E090301", "해당 매칭이 존재하지 않습니다."), CHAT_ROOM_NOT_FOUND("E110301", "해당 채팅방이 존재하지 않습니다."), + ACHIEVEMENT_NOT_FOUND("E130301", "해당 업적이 존재하지 않습니다."), ; private final HttpStatus httpStatus = HttpStatus.NOT_FOUND; diff --git a/src/main/java/io/oeid/mogakgo/scheduler/FinishedProjectScheduler.java b/src/main/java/io/oeid/mogakgo/scheduler/FinishedProjectScheduler.java index 2fd3785b..e1dc7a9d 100644 --- a/src/main/java/io/oeid/mogakgo/scheduler/FinishedProjectScheduler.java +++ b/src/main/java/io/oeid/mogakgo/scheduler/FinishedProjectScheduler.java @@ -4,47 +4,47 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +@Slf4j @Component public class FinishedProjectScheduler { private final JdbcTemplate jdbcTemplate; - private final ResourceLoader resourceLoader; + private final Resource[] sqlStatements; - private final List sqlFilePaths; - - public FinishedProjectScheduler( - JdbcTemplate jdbcTemplate, ResourceLoader resourceLoader, - @Value("${path.sql.finished-matched-project}") String finishedMatchedProjectSqlPath, - @Value("${path.sql.finished-pending-project}") String finishedPendingProjectSqlPath, - @Value("${path.sql.finished-project-request}") String finishedProjectRequestSqlPath - ) { + private FinishedProjectScheduler( + JdbcTemplate jdbcTemplate, + @Qualifier("webApplicationContext") ResourcePatternResolver resourcePatternResolver, + @Value("${path.schedule.sql}") String sqlStatementsPath + ) throws IOException { this.jdbcTemplate = jdbcTemplate; - this.resourceLoader = resourceLoader; - this.sqlFilePaths = List.of(finishedMatchedProjectSqlPath, finishedPendingProjectSqlPath, - finishedProjectRequestSqlPath); + this.sqlStatements = resourcePatternResolver.getResources(sqlStatementsPath); } - @Scheduled(cron = "0 0 0 * * ?") // 매일 자정에 실행 - public void executeSqlFile() throws IOException { - for (String sqlFilePath : sqlFilePaths) { - String sql = loadSqlFromFile(sqlFilePath); + @Scheduled(cron = "* * * * * ?") // 매일 자정에 실행 + public void executeSqlFile() { + for (Resource statement : sqlStatements) { + String sql = loadSqlFromFile(statement); jdbcTemplate.execute(sql); } } - private String loadSqlFromFile(String path) throws IOException { + private String loadSqlFromFile(Resource resource) { try (BufferedReader reader = new BufferedReader( - new InputStreamReader(resourceLoader.getResource(path).getInputStream(), - StandardCharsets.UTF_8))) { + new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) { return reader.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + log.error("sql 스케쥴링 실행 중 오류 발생 : " + resource.getFilename(), e); + return ""; } } diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 00000000..02c0d831 --- /dev/null +++ b/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,162 @@ +CREATE TABLE IF NOT EXISTS user_tb ( + id BIGINT AUTO_INCREMENT, + available_join_count INT, + available_like_count INT, + avatar_url VARCHAR(255), + bio VARCHAR(50), + created_at DATETIME(6), + deleted_at DATETIME(6), + github_id VARCHAR(255), + github_pk BIGINT, + github_url VARCHAR(255), + jandi_rate DOUBLE, + region ENUM('JONGNO', 'JUNG', 'YONGSAN', 'SEONGDONG', 'GWANGJIN', 'DONGDAEMUN', 'JUNGNANG', 'SEONGBUK', 'GANGBUK', 'DOBONG', 'NOWON', 'EUNPYEONG', 'SEODAEMUN', 'MAPO', 'YANGCHEON', 'GANGSEO', 'GURO', 'GEUMCHEON', 'YOUNGDEUNGPO', 'DONGJAK', 'GWANAK', 'SEOCHO', 'GANGNAM', 'SONGPA', 'GANGDONG', 'BUNDANG'), + region_authenticated_at datetime(6), + role ENUM('ROLE_USER'), + username VARCHAR(255), + signup_yn TINYINT, + achievement_id BIGINT, + repository_url VARCHAR(255), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS user_wanted_job_tb ( + id BIGINT AUTO_INCREMENT, + wanted_job ENUM('BACKEND', 'FRONTEND', 'FULLSTACK', 'ANDROID', 'IOS', 'MACHINE_LEARNING', 'ARTIFICIAL_INTELLIGENCE', 'DATA_ENGINEER', 'DBA', 'MOBILE_GAME', 'SYSTEM_NETWORK', 'SYSTEM_SW', 'DEVOPS', 'INTERNET_SECURITY', 'EMBEDDED_SOFTWARE', 'ROBOTICS_MIDDLEWARE', 'QA', 'IOT', 'APPLICATION_SW', 'BLOCKCHAIN', 'PROJECT_MANAGEMENT', 'WEB_PUBLISHING', 'CROSS_PLATFORM', 'VR_AR_3D', 'ERP', 'GRAPHICS'), + user_id BIGINT, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS user_develop_language_tb ( + id BIGINT AUTO_INCREMENT, + byte_size INT, + develop_language ENUM('PYTHON', 'JAVA', 'JAVASCRIPT', 'C', 'CPP', 'CSHARP', 'RUBY', 'SWIFT', 'KOTLIN', 'GO', 'TYPESCRIPT', 'SCALA', 'RUST', 'PHP', 'HTML', 'CSS', 'ELM', 'ERLANG', 'HASKELL', 'R', 'SHELL', 'SQL', 'DART', 'OBJECT_C', 'ETC'), + user_id BIGINT, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS user_achievement_tb ( + id BIGINT AUTO_INCREMENT, + user_id BIGINT, + achievement_id BIGINT, + completed TINYINT(1), + created_at TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS user_activity_tb ( + id BIGINT AUTO_INCREMENT, + user_id BIGINT, + activity_type ENUM('FROM_ONE_STEP', 'GOOD_PERSON_GOOD_MEETUP', 'LIKE_E', 'MY_DESTINY', 'CAPTURE_FAIL_EXIST', 'RUN_AWAY_FROM_MONSTER_BALL', 'PLEASE_GIVE_ME_MOGAK', 'BRAVE_EXPLORER', 'NOMAD_CODER', 'CATCH_ME_IF_YOU_CAN', 'LEAVE_YOUR_MARK', 'WHAT_A_POPULAR_PERSON', 'CONTACT_WITH_GOD', 'FRESH_DEVELOPER'), + created_at TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS project_tb ( + id BIGINT AUTO_INCREMENT, + creator_id BIGINT, + bio VARCHAR(255), + jandi_rating DOUBLE, + avatar_url VARCHAR(255), + region ENUM('JONGNO', 'JUNG', 'YONGSAN', 'SEONGDONG', 'GWANGJIN', 'DONGDAEMUN', 'JUNGNANG', 'SEONGBUK', 'GANGBUK', 'DOBONG', 'NOWON', 'EUNPYEONG', 'SEODAEMUN', 'MAPO', 'YANGCHEON', 'GANGSEO', 'GURO', 'GEUMCHEON', 'YOUNGDEUNGPO', 'DONGJAK', 'GWANAK', 'SEOCHO', 'GANGNAM', 'SONGPA', 'GANGDONG', 'BUNDANG'), + username VARCHAR(255), + meet_start_time TIMESTAMP, + meet_end_time TIMESTAMP, + meet_location POINT, + meet_detail VARCHAR(255), + creator_github_id VARCHAR(255), + main_achievement_id BIGINT, + project_status ENUM('PENDING', 'MATCHED', 'CANCELED', 'FINISHED'), + created_at TIMESTAMP, + deleted_at TIMESTAMP, + user_github_id VARCHAR(255), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS project_tag_tb ( + id BIGINT AUTO_INCREMENT, + project_id BIGINT, + content VARCHAR(255), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS profile_card_tb ( + id BIGINT AUTO_INCREMENT, + user_id BIGINT, + total_like_amount BIGINT, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS project_join_request_tb ( + id BIGINT AUTO_INCREMENT, + sender_id BIGINT, + project_id BIGINT, + join_request_status ENUM('PENDING', 'ACCEPTED', 'REJECTED', 'CANCELED'), + created_at TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS matching_tb ( + id BIGINT AUTO_INCREMENT, + created_at TIMESTAMP, + project_id BIGINT, + sender_id BIGINT, + matching_status ENUM('PROGRESS', 'CANCELED', 'FINISHED'), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS profile_card_like_tb ( + id BIGINT AUTO_INCREMENT, + sender_id BIGINT, + receiver_id BIGINT, + created_at TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS review_tb ( + id BIGINT AUTO_INCREMENT, + sender_id BIGINT, + receiver_id BIGINT, + project_id BIGINT, + rating ENUM('1', '2', '3', '4', '5'), + created_at TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS achievement_tb ( + id BIGINT AUTO_INCREMENT, + title VARCHAR(255), + img_url VARCHAR(255), + description VARCHAR(255), + progress_level INT, + requirement_type ENUM('SEQUENCE', 'ACCUMULATE'), + requirement_value INT, + activity_type ENUM('FROM_ONE_STEP', 'GOOD_PERSON_GOOD_MEETUP', 'LIKE_E', 'MY_DESTINY', 'CAPTURE_FAIL_EXIST', 'RUN_AWAY_FROM_MONSTER_BALL', 'PLEASE_GIVE_ME_MOGAK', 'BRAVE_EXPLORER', 'NOMAD_CODER', 'CATCH_ME_IF_YOU_CAN', 'LEAVE_YOUR_MARK', 'WHAT_A_POPULAR_PERSON', 'CONTACT_WITH_GOD', 'FRESH_DEVELOPER'), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS chat_room_tb ( + id BIGINT AUTO_INCREMENT, + project_id BIGINT, + creator_id BIGINT, + sender_id BIGINT, + status ENUM('OPEN', 'CLOSE'), + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS notification_tb ( + id BIGINT AUTO_INCREMENT, + created_at DATETIME(6), + detail_data VARCHAR(255), + tag ENUM('ACHIEVEMENT', 'REQUEST_ARRIVAL', 'MATCHING_FINISHED', 'MATCHING_SUCCEEDED', 'MATCHING_FAILED', 'REVIEW_REQUEST'), + sender_id BIGINT, + receiver_id BIGINT, + checked_yn TINYINT, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS fcm_token_tb ( + id BIGINT, + token VARCHAR(255), + PRIMARY KEY (id) +); diff --git a/src/main/resources/sql/finished_matched_project_after_12.sql b/src/main/resources/schedule_sql/finished_matched_project_after_12.sql similarity index 100% rename from src/main/resources/sql/finished_matched_project_after_12.sql rename to src/main/resources/schedule_sql/finished_matched_project_after_12.sql diff --git a/src/main/resources/sql/finished_pending_project_after_12.sql b/src/main/resources/schedule_sql/finished_pending_project_after_12.sql similarity index 100% rename from src/main/resources/sql/finished_pending_project_after_12.sql rename to src/main/resources/schedule_sql/finished_pending_project_after_12.sql diff --git a/src/main/resources/schedule_sql/finished_progress_matching_after_12.sql b/src/main/resources/schedule_sql/finished_progress_matching_after_12.sql new file mode 100644 index 00000000..ce05f383 --- /dev/null +++ b/src/main/resources/schedule_sql/finished_progress_matching_after_12.sql @@ -0,0 +1,3 @@ +UPDATE matching_tb +SET matching_status = 'FINISHED' +WHERE DATE(created_at) != CURDATE(); diff --git a/src/main/resources/sql/rejected_project_request_about_finished_project.sql b/src/main/resources/schedule_sql/rejected_project_request_about_finished_project.sql similarity index 100% rename from src/main/resources/sql/rejected_project_request_about_finished_project.sql rename to src/main/resources/schedule_sql/rejected_project_request_about_finished_project.sql diff --git a/src/test/java/io/oeid/mogakgo/core/support/WithMockCustomUser.java b/src/test/java/io/oeid/mogakgo/core/support/WithMockCustomUser.java new file mode 100644 index 00000000..ace88ee6 --- /dev/null +++ b/src/test/java/io/oeid/mogakgo/core/support/WithMockCustomUser.java @@ -0,0 +1,11 @@ +package io.oeid.mogakgo.core.support; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) +public @interface WithMockCustomUser { + long userId() default 1L; +} diff --git a/src/test/java/io/oeid/mogakgo/core/support/WithMockCustomUserSecurityContextFactory.java b/src/test/java/io/oeid/mogakgo/core/support/WithMockCustomUserSecurityContextFactory.java new file mode 100644 index 00000000..de547fe7 --- /dev/null +++ b/src/test/java/io/oeid/mogakgo/core/support/WithMockCustomUserSecurityContextFactory.java @@ -0,0 +1,44 @@ +package io.oeid.mogakgo.core.support; + +import io.oeid.mogakgo.core.properties.JwtProperties; +import io.oeid.mogakgo.domain.auth.jwt.JwtAuthenticationToken; +import io.oeid.mogakgo.domain.auth.jwt.JwtHelper; +import java.util.List; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockCustomUserSecurityContextFactory implements + WithSecurityContextFactory { + + private static JwtProperties initProperties() { + JwtProperties properties = new JwtProperties(); + properties.setHeader("test"); + properties.setIssuer("test"); + properties.setClientSecret("test"); + properties.setAccessTokenExpiryHour(1); + properties.setRefreshTokenExpiryHour(3); + return properties; + } + + private final JwtHelper jwtHelper; + + public WithMockCustomUserSecurityContextFactory() { + this.jwtHelper = new JwtHelper( + initProperties() + ); + } + + @Override + public SecurityContext createSecurityContext(WithMockCustomUser mockCustomUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + List authorities = List.of(new SimpleGrantedAuthority("ROLE_USER")); + var token = jwtHelper.sign(mockCustomUser.userId(), + authorities.stream().map(GrantedAuthority::getAuthority).toArray(String[]::new)); + var auth = new JwtAuthenticationToken(token, null, authorities); + context.setAuthentication(auth); + return context; + } +}