From 9601930c66c648a90c7d97a63511042a60f59f54 Mon Sep 17 00:00:00 2001 From: Bae Hyeonseo Date: Wed, 22 May 2024 00:45:59 +0900 Subject: [PATCH] =?UTF-8?q?Global=20=EC=84=9C=EB=B2=84=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature_request.md | 25 ++ ...0-\355\205\234\355\224\214\353\246\277.md" | 34 ++ .github/pull_request_template.md | 38 ++ .github/workflows/main-service-ci.yml | 45 ++ .github/workflows/main-service-cicd.yml | 87 ++++ .github/workflows/socket-service-ci.yml | 46 ++ .github/workflows/socket-service-cicd.yml | 85 ++++ .idea/.gitignore | 3 + .idea/dbnavigator.xml | 414 ++++++++++++++++++ .idea/modules.xml | 8 + ".idea/\354\213\261\355\201\254.iml" | 9 + Main/.gitignore | 42 ++ Main/Dockerfile | 3 + Main/build.gradle | 26 ++ Main/docker-compose.yml | 20 + Main/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes Main/gradle/wrapper/gradle-wrapper.properties | 7 + Main/gradlew | 249 +++++++++++ Main/gradlew.bat | 92 ++++ Main/settings.gradle | 1 + .../backendH/BackendHApplication.java | 13 + .../kusitms29/backendH/TestController.java | 12 + .../controller/CommunityController.java | 144 ++++++ .../community/service/CommentLikeService.java | 50 +++ .../api/community/service/CommentService.java | 145 ++++++ .../community/service/PostLikeService.java | 52 +++ .../community/service/PostSearchService.java | 63 +++ .../api/community/service/PostService.java | 160 +++++++ .../community/service/ReplyLikeService.java | 50 +++ .../api/community/service/ReplyService.java | 59 +++ .../dto/request/CommentCreateRequestDto.java | 12 + .../service/dto/request/PostCalculateDto.java | 13 + .../dto/request/PostCreateRequestDto.java | 13 + .../dto/response/BannerImageResponseDto.java | 16 + .../response/CommentCreateResponseDto.java | 31 ++ .../dto/response/CommentResponseDto.java | 42 ++ .../dto/response/PostCreateResponseDto.java | 25 ++ .../dto/response/PostDetailResponseDto.java | 45 ++ .../service/dto/response/PostResponseDto.java | 44 ++ .../dto/response/PostSearchResponseDto.java | 43 ++ .../dto/response/ReplyCreateResponseDto.java | 31 ++ .../dto/response/ReplyResponseDto.java | 38 ++ .../NotificationHistoryController.java | 29 ++ .../service/NotificationHistoryService.java | 72 +++ .../service/NotificationService.java | 36 ++ .../dto/NotificationHistoryResponseDto.java | 30 ++ .../sync/controller/SyncDetailController.java | 48 ++ .../sync/controller/SyncMainController.java | 49 +++ .../sync/controller/SyncManageController.java | 34 ++ .../api/sync/service/SyncDetailService.java | 132 ++++++ .../api/sync/service/SyncService.java | 233 ++++++++++ .../dto/request/SyncCreateRequestDto.java | 29 ++ .../dto/request/SyncInfoRequestDto.java | 13 + .../service/dto/response/GraphElement.java | 17 + .../dto/response/SeoulAddressResponse.java | 16 + .../SyncAssociateInfoResponseDto.java | 36 ++ .../dto/response/SyncCreateResponseDto.java | 45 ++ .../dto/response/SyncDetailResponseDto.java | 66 +++ .../dto/response/SyncGraphResponseDto.java | 13 + .../dto/response/SyncInfoResponseDto.java | 34 ++ .../dto/response/SyncReviewResponseDto.java | 25 ++ .../dto/response/SyncSaveResponseDto.java | 61 +++ .../api/user/controller/AuthController.java | 23 + .../api/user/controller/MyPageController.java | 55 +++ .../user/controller/OnBoardingController.java | 69 +++ .../api/user/service/AuthService.java | 104 +++++ .../api/user/service/MyPageService.java | 113 +++++ .../api/user/service/OnBoardingService.java | 76 ++++ .../api/user/service/UserService.java | 23 + .../dto/request/CategoryRequestDto.java | 18 + .../dto/request/CountryCalloutRequestDto.java | 16 + .../dto/request/CreateReviewRequest.java | 7 + .../dto/request/EditProfileRequest.java | 14 + .../request/EmailVerificationRequestDto.java | 12 + .../dto/request/OnBoardingRequestDto.java | 20 + .../dto/request/ReceiverInfoRequestDto.java | 11 + .../dto/request/UniversityRequestDto.java | 11 + .../dto/request/UserSignInRequestDto.java | 11 + .../CalloutSchoolEmailRequestDto.java | 14 + ...loutSchoolEmailVerificationRequestDto.java | 14 + .../schoolEmail/SchoolEmailRequestDto.java | 12 + .../SchoolEmailVerificationRequestDto.java | 13 + .../service/dto/response/CountryDataDto.java | 17 + .../dto/response/CountryResponseDto.java | 17 + .../dto/response/CreateReviewResponse.java | 12 + .../EmailVerificationResponseDto.java | 17 + .../dto/response/OnBoardingResponseDto.java | 35 ++ .../dto/response/UserAuthResponseDto.java | 32 ++ .../dto/response/UserInfoResponseDto.java | 39 ++ .../dto/response/UserSignUpResponseDto.java | 16 + .../schoolEmail/CalloutErrorResponse.java | 14 + ...outSchoolEmailVerificationResponseDto.java | 13 + .../domain/category/entity/Category.java | 22 + .../backendH/domain/category/entity/Type.java | 37 ++ .../domain/category/entity/UserCategory.java | 34 ++ .../repository/CategoryRepository.java | 9 + .../repository/UserCategoryRepository.java | 11 + .../category/service/CategoryReader.java | 24 + .../category/service/UserCategoryManager.java | 16 + .../service/UserCategoryModifier.java | 26 ++ .../category/service/UserCategoryReader.java | 17 + .../domain/chat/entity/ChatContent.java | 24 + .../backendH/domain/chat/entity/ChatUser.java | 21 + .../backendH/domain/chat/entity/Room.java | 56 +++ .../chat/repository/RoomRepository.java | 7 + .../domain/chat/service/RoomAppender.java | 40 ++ .../domain/comment/entity/Comment.java | 35 ++ .../domain/comment/entity/CommentLike.java | 27 ++ .../backendH/domain/comment/entity/Reply.java | 34 ++ .../domain/comment/entity/ReplyLike.java | 26 ++ .../repository/CommentLikeRepository.java | 21 + .../comment/repository/CommentRepository.java | 19 + .../repository/ReplyLikeRepository.java | 20 + .../comment/repository/ReplyRepository.java | 23 + .../comment/service/CommentLikeManager.java | 18 + .../comment/service/CommentLikeModifier.java | 28 ++ .../comment/service/CommentLikeReader.java | 35 ++ .../comment/service/CommentManager.java | 18 + .../comment/service/CommentModifier.java | 28 ++ .../domain/comment/service/CommentReader.java | 29 ++ .../comment/service/ReplyLikeManager.java | 18 + .../comment/service/ReplyLikeModifier.java | 28 ++ .../comment/service/ReplyLikeReader.java | 34 ++ .../domain/comment/service/ReplyManager.java | 19 + .../domain/comment/service/ReplyModifier.java | 32 ++ .../domain/comment/service/ReplyReader.java | 29 ++ .../entity/NotificationHistory.java | 56 +++ .../entity/NotificationSetting.java | 37 ++ .../notification/entity/NotificationType.java | 9 + .../notification/entity/TopCategory.java | 25 ++ .../NotificationHistoryRepository.java | 13 + .../repository/NotificationRepository.java | 12 + .../service/NotificationHistoryReader.java | 20 + .../backendH/domain/post/entity/Post.java | 28 ++ .../domain/post/entity/PostImage.java | 26 ++ .../backendH/domain/post/entity/PostLike.java | 28 ++ .../backendH/domain/post/entity/PostType.java | 23 + .../post/repository/PostImageRepository.java | 13 + .../post/repository/PostLikeRepository.java | 16 + .../post/repository/PostRepository.java | 19 + .../domain/post/service/PostAppender.java | 22 + .../post/service/PostImageAppender.java | 21 + .../domain/post/service/PostImageReader.java | 27 ++ .../domain/post/service/PostLikeAppender.java | 22 + .../domain/post/service/PostLikeManager.java | 22 + .../domain/post/service/PostLikeModifier.java | 20 + .../domain/post/service/PostLikeReader.java | 24 + .../domain/post/service/PostReader.java | 37 ++ .../domain/sync/entity/FavoriteSync.java | 27 ++ .../backendH/domain/sync/entity/Gender.java | 28 ++ .../backendH/domain/sync/entity/Language.java | 26 ++ .../domain/sync/entity/Participation.java | 34 ++ .../backendH/domain/sync/entity/Sync.java | 99 +++++ .../domain/sync/entity/SyncReview.java | 39 ++ .../domain/sync/entity/SyncStatus.java | 15 + .../backendH/domain/sync/entity/SyncType.java | 32 ++ .../repository/FavoriteSyncRepository.java | 10 + .../repository/ParticipationRepository.java | 20 + .../sync/repository/SyncRepository.java | 56 +++ .../sync/repository/SyncReviewRepository.java | 10 + .../sync/service/FavoriteSyncReader.java | 18 + .../sync/service/ParticipationManager.java | 14 + .../sync/service/ParticipationReader.java | 25 ++ .../domain/sync/service/SyncAppender.java | 15 + .../domain/sync/service/SyncManager.java | 185 ++++++++ .../domain/sync/service/SyncReader.java | 187 ++++++++ .../sync/service/SyncReviewAppender.java | 18 + .../domain/sync/service/SyncReviewReader.java | 17 + .../domain/user/auth/PlatformUserInfo.java | 22 + .../user/auth/RestTemplateProvider.java | 62 +++ .../user/auth/google/GoogleAuthProvider.java | 49 +++ .../domain/user/auth/google/GoogleToken.java | 20 + .../user/auth/google/GoogleUserInfo.java | 23 + .../user/auth/kakao/KakaoAuthProvider.java | 48 ++ .../domain/user/auth/kakao/KakaoToken.java | 20 + .../domain/user/auth/kakao/KakaoUserInfo.java | 29 ++ .../backendH/domain/user/entity/Coupon.java | 29 ++ .../backendH/domain/user/entity/Platform.java | 28 ++ .../domain/user/entity/RefreshToken.java | 25 ++ .../backendH/domain/user/entity/User.java | 89 ++++ .../backendH/domain/user/ip/IpService.java | 41 ++ .../repository/RefreshTokenRepository.java | 9 + .../user/repository/UserRepository.java | 12 + .../domain/user/service/UserModifier.java | 16 + .../domain/user/service/UserReader.java | 24 + .../backendH/global/common/BaseEntity.java | 27 ++ .../common/HealthCheckApiController.java | 141 ++++++ .../backendH/global/common/SuccessCode.java | 24 + .../global/common/SuccessResponse.java | 37 ++ .../global/common/TimeCalculator.java | 76 ++++ .../backendH/global/common/TimeConverter.java | 23 + .../backendH/global/error/ErrorCode.java | 110 +++++ .../global/error/dto/ErrorBaseResponse.java | 23 + .../error/exception/BusinessException.java | 20 + .../error/exception/ConflictException.java | 14 + .../exception/EntityNotFoundException.java | 14 + .../error/exception/ForbiddenException.java | 14 + .../exception/InternalServerException.java | 10 + .../exception/InvalidValueException.java | 14 + .../global/error/exception/ListException.java | 13 + .../error/exception/NotAllowedException.java | 14 + .../exception/UnauthorizedException.java | 14 + .../error/handler/GlobalExceptionHandler.java | 80 ++++ .../backendH/infra/config/AwsS3Config.java | 30 ++ .../backendH/infra/config/AwsS3Service.java | 92 ++++ .../backendH/infra/config/CorsConfig.java | 25 ++ .../backendH/infra/config/EmailConfig.java | 68 +++ .../backendH/infra/config/FCMConfig.java | 38 ++ .../backendH/infra/config/JpaConfig.java | 10 + .../backendH/infra/config/RedisConfig.java | 48 ++ .../backendH/infra/config/RedisService.java | 28 ++ .../infra/config/RestTemplateConfig.java | 22 + .../backendH/infra/config/ScheduleConfig.java | 9 + .../backendH/infra/config/SecurityConfig.java | 52 +++ .../backendH/infra/config/WebConfig.java | 28 ++ .../config/auth/ExceptionHandlerFilter.java | 54 +++ .../auth/JwtAuthenticationEntryPoint.java | 30 ++ .../config/auth/JwtAuthenticationFilter.java | 48 ++ .../infra/config/auth/JwtProvider.java | 86 ++++ .../backendH/infra/config/auth/TokenInfo.java | 17 + .../infra/config/auth/UserAuthentication.java | 12 + .../backendH/infra/config/auth/UserId.java | 12 + .../config/auth/UserIdArgumentResolver.java | 26 ++ .../infra/external/CountryDataClient.java | 60 +++ .../infra/external/SchoolEmailClient.java | 99 +++++ .../infra/external/SeoulAddressClient.java | 47 ++ .../infra/external/UniversityClient.java | 45 ++ .../infra/external/clova/map/GeoLocation.java | 18 + .../clova/map/GeoLocationResponse.java | 10 + .../clova/map/GeoLocationService.java | 120 +++++ .../external/clova/papago/PapagoService.java | 99 +++++ .../detection/LanguageDetectionRequest.java | 8 + .../detection/LanguageDetectionResponse.java | 8 + .../translation/TextTranslationRequest.java | 12 + .../translation/TextTranslationResponse.java | 20 + .../infra/external/fcm/FCMScheduler.java | 21 + .../infra/external/fcm/MessageTemplate.java | 20 + .../fcm/repository/FCMTokenRepository.java | 28 ++ .../fcm/service/PushNotificationService.java | 235 ++++++++++ .../fcm/service/dto/NotificationDto.java | 71 +++ .../fcm/service/dto/SyncReminderDto.java | 18 + .../backendH/infra/utils/ListUtils.java | 16 + .../backendH/infra/utils/TranslateUtil.java | 61 +++ .../backendH/BackendHApplicationTests.java | 13 + scripts/deploy.sh | 53 +++ socket/.gitignore | 42 ++ socket/Dockerfile | 3 + socket/build.gradle | 26 ++ socket/docker-compose.yml | 20 + socket/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + socket/gradlew | 249 +++++++++++ socket/gradlew.bat | 92 ++++ socket/settings.gradle | 1 + .../Backend/socket/SocketApplication.java | 13 + .../controller/ChatController.java | 82 ++++ .../controller/RoomController.java | 57 +++ .../dto/request/ChatListRequestDto.java | 11 + .../request/ChatMessageListRequestDto.java | 13 + .../dto/request/ChatMessageRequestDto.java | 14 + .../request/ChatMessageRoomRequestDto.java | 18 + .../dto/request/RoomChatMessageListReq.java | 14 + .../request/RoomMessageListRequestDto.java | 13 + .../dto/response/ChatListResponseDto.java | 18 + .../ChatMessageElementResponseDto.java | 42 ++ .../response/ChatMessageListResponseDto.java | 20 + .../dto/response/ChatMessageResponseDto.java | 22 + .../response/ChatMessageRoomResponseDto.java | 22 + .../dto/response/ChatUserResponseDto.java | 25 ++ .../dto/response/RoomChatResponseDto.java | 39 ++ .../dto/response/RoomListResponseDto.java | 19 + .../RoomMessageElementResponseDto.java | 44 ++ .../response/RoomMessageListResponseDto.java | 19 + .../dto/response/SendMessageResponseDto.java | 18 + .../dto/response/UserChatResponseDto.java | 27 ++ .../chat/application/service/ChatService.java | 241 ++++++++++ .../chat/application/service/Formatter.java | 29 ++ .../application/service/RedisSubscriber.java | 78 ++++ .../chat/application/service/RoomService.java | 89 ++++ .../chat/application/service/TriFunction.java | 6 + .../socket/domain/chat/domain/Chat.java | 37 ++ .../domain/chat/domain/ChatContent.java | 33 ++ .../socket/domain/chat/domain/ChatUser.java | 20 + .../socket/domain/chat/domain/Gender.java | 15 + .../socket/domain/chat/domain/Language.java | 13 + .../socket/domain/chat/domain/Platform.java | 26 ++ .../socket/domain/chat/domain/Room.java | 56 +++ .../socket/domain/chat/domain/SyncType.java | 25 ++ .../socket/domain/chat/domain/User.java | 47 ++ .../entity/NotificationHistory.java | 56 +++ .../notification/entity/NotificationType.java | 9 + .../notification/entity/TopCategory.java | 26 ++ .../NotificationHistoryRepository.java | 12 + .../chat/repository/ChatRepository.java | 8 + .../chat/repository/RoomRepository.java | 7 + .../chat/repository/UserRepository.java | 10 + .../common/AuthenticationInterceptor.java | 66 +++ .../socket/global/common/BaseEntity.java | 27 ++ .../socket/global/common/HealthCheck.java | 100 +++++ .../socket/global/common/JwtProvider.java | 57 +++ .../global/common/MessageSuccessCode.java | 17 + .../global/common/MessageSuccessResponse.java | 23 + .../Backend/socket/global/common/image.java | 11 + .../socket/global/common/imageList.java | 4 + .../socket/global/error/ErrorCode.java | 42 ++ .../global/error/dto/ErrorBaseResponse.java | 23 + .../error/handler/MessageErrorHandler.java | 36 ++ .../httpException/BusinessException.java | 20 + .../InternalServerException.java | 10 + .../httpException/InvalidValueException.java | 10 + .../httpException/UnauthorizedException.java | 11 + .../socketException/BusinessException.java | 15 + .../EntityNotFoundException.java | 9 + .../UnauthorizedException.java | 10 + ...AgentWebSocketHandlerDecoratorFactory.java | 21 + .../socket/infra/config/AwsS3Config.java | 30 ++ .../socket/infra/config/FCMConfig.java | 38 ++ .../socket/infra/config/MongoDbConfig.java | 30 ++ .../socket/infra/config/RedisConfig.java | 104 +++++ .../socket/infra/config/SocketConfig.java | 52 +++ .../infra/config/SocketSecurityConfig.java | 32 ++ ...ubProtocolWebSocketHandlerInterceptor.java | 16 + .../socket/infra/config/WebConfig.java | 25 ++ .../config/auth/ExceptionHandlerFilter.java | 53 +++ .../infra/config/auth/IgnorePathConsts.java | 29 ++ .../auth/JwtAuthenticationEntryPoint.java | 32 ++ .../config/auth/JwtAuthenticationFilter.java | 87 ++++ .../infra/config/auth/SecurityConfig.java | 65 +++ .../infra/config/auth/UserAuthentication.java | 12 + .../socket/infra/config/auth/UserId.java | 11 + .../config/auth/UserIdArgumentResolver.java | 24 + .../socket/infra/external/AwsService.java | 143 ++++++ .../infra/external/fcm/MessageTemplate.java | 20 + .../fcm/repository/FCMTokenRepository.java | 28 ++ .../fcm/service/PushNotificationService.java | 134 ++++++ .../fcm/service/dto/NotificationDto.java | 46 ++ .../socket/SocketApplicationTests.java | 13 + 337 files changed, 12861 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 ".github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/main-service-ci.yml create mode 100644 .github/workflows/main-service-cicd.yml create mode 100644 .github/workflows/socket-service-ci.yml create mode 100644 .github/workflows/socket-service-cicd.yml create mode 100644 .idea/.gitignore create mode 100644 .idea/dbnavigator.xml create mode 100644 .idea/modules.xml create mode 100644 ".idea/\354\213\261\355\201\254.iml" create mode 100644 Main/.gitignore create mode 100644 Main/Dockerfile create mode 100644 Main/build.gradle create mode 100644 Main/docker-compose.yml create mode 100644 Main/gradle/wrapper/gradle-wrapper.jar create mode 100644 Main/gradle/wrapper/gradle-wrapper.properties create mode 100644 Main/gradlew create mode 100644 Main/gradlew.bat create mode 100644 Main/settings.gradle create mode 100644 Main/src/main/java/com/kusitms29/backendH/BackendHApplication.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/TestController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/controller/CommunityController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentLikeService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/PostLikeService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/PostSearchService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/PostService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/ReplyLikeService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/ReplyService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/CommentCreateRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCalculateDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCreateRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/BannerImageResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentCreateResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostCreateResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostDetailResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostSearchResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyCreateResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/notification/controller/NotificationHistoryController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationHistoryService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/notification/service/dto/NotificationHistoryResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncDetailController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncMainController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncManageController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncDetailService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncCreateRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncInfoRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/GraphElement.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SeoulAddressResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncAssociateInfoResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncCreateResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncDetailResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncGraphResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncInfoResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncReviewResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncSaveResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/controller/AuthController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/controller/MyPageController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/controller/OnBoardingController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/AuthService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/MyPageService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/OnBoardingService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/UserService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CategoryRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CountryCalloutRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CreateReviewRequest.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EditProfileRequest.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EmailVerificationRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/OnBoardingRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/ReceiverInfoRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UniversityRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UserSignInRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailVerificationRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailVerificationRequestDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryDataDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CreateReviewResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/EmailVerificationResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/OnBoardingResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserAuthResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserInfoResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserSignUpResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutErrorResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutSchoolEmailVerificationResponseDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Category.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Type.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/entity/UserCategory.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/repository/CategoryRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/repository/UserCategoryRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/service/CategoryReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatContent.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatUser.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/Room.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/chat/repository/RoomRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/chat/service/RoomAppender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Comment.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/CommentLike.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Reply.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/ReplyLike.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentLikeRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyLikeRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationHistory.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationSetting.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationType.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/TopCategory.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationHistoryRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/notification/service/NotificationHistoryReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/entity/Post.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostImage.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostLike.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostType.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostImageRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostLikeRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostAppender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageAppender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeAppender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/FavoriteSync.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Gender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Language.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Participation.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Sync.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncReview.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncStatus.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncType.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/FavoriteSyncRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/ParticipationRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncReviewRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/FavoriteSyncReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncAppender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncManager.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewAppender.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/PlatformUserInfo.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/RestTemplateProvider.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleAuthProvider.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleToken.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleUserInfo.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoAuthProvider.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoToken.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoUserInfo.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Coupon.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Platform.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/entity/RefreshToken.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/entity/User.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/ip/IpService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/repository/RefreshTokenRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/repository/UserRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserModifier.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserReader.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/common/BaseEntity.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/common/HealthCheckApiController.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/common/SuccessCode.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/common/SuccessResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/common/TimeCalculator.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/common/TimeConverter.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/ErrorCode.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/dto/ErrorBaseResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/BusinessException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/ConflictException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/EntityNotFoundException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/ForbiddenException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/InternalServerException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/InvalidValueException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/ListException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/NotAllowedException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/exception/UnauthorizedException.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/global/error/handler/GlobalExceptionHandler.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Config.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Service.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/CorsConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/EmailConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/FCMConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/JpaConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/RedisConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/RedisService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/RestTemplateConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/ScheduleConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/SecurityConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/WebConfig.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/ExceptionHandlerFilter.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationEntryPoint.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationFilter.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtProvider.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/TokenInfo.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserAuthentication.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserId.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserIdArgumentResolver.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/CountryDataClient.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/SchoolEmailClient.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/SeoulAddressClient.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/UniversityClient.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocation.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/PapagoService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionRequest.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationRequest.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationResponse.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/FCMScheduler.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/MessageTemplate.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/repository/FCMTokenRepository.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/PushNotificationService.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/NotificationDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/SyncReminderDto.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/utils/ListUtils.java create mode 100644 Main/src/main/java/com/kusitms29/backendH/infra/utils/TranslateUtil.java create mode 100644 Main/src/test/java/com/kusitms29/backendH/BackendHApplicationTests.java create mode 100644 scripts/deploy.sh create mode 100644 socket/.gitignore create mode 100644 socket/Dockerfile create mode 100644 socket/build.gradle create mode 100644 socket/docker-compose.yml create mode 100644 socket/gradle/wrapper/gradle-wrapper.jar create mode 100644 socket/gradle/wrapper/gradle-wrapper.properties create mode 100644 socket/gradlew create mode 100644 socket/gradlew.bat create mode 100644 socket/settings.gradle create mode 100644 socket/src/main/java/Backend/socket/SocketApplication.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/ChatController.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/RoomController.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatListRequestDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageListRequestDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRequestDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRoomRequestDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomChatMessageListReq.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomMessageListRequestDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatListResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageElementResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageListResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageRoomResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatUserResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomChatResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomListResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageElementResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageListResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/SendMessageResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/UserChatResponseDto.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/service/ChatService.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/service/Formatter.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/service/RedisSubscriber.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/service/RoomService.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/application/service/TriFunction.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/Chat.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/ChatContent.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/ChatUser.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/Gender.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/Language.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/Platform.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/Room.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/SyncType.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/User.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationHistory.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationType.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/TopCategory.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/domain/notification/repository/NotificationHistoryRepository.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/repository/ChatRepository.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/repository/RoomRepository.java create mode 100644 socket/src/main/java/Backend/socket/domain/chat/repository/UserRepository.java create mode 100644 socket/src/main/java/Backend/socket/global/common/AuthenticationInterceptor.java create mode 100644 socket/src/main/java/Backend/socket/global/common/BaseEntity.java create mode 100644 socket/src/main/java/Backend/socket/global/common/HealthCheck.java create mode 100644 socket/src/main/java/Backend/socket/global/common/JwtProvider.java create mode 100644 socket/src/main/java/Backend/socket/global/common/MessageSuccessCode.java create mode 100644 socket/src/main/java/Backend/socket/global/common/MessageSuccessResponse.java create mode 100644 socket/src/main/java/Backend/socket/global/common/image.java create mode 100644 socket/src/main/java/Backend/socket/global/common/imageList.java create mode 100644 socket/src/main/java/Backend/socket/global/error/ErrorCode.java create mode 100644 socket/src/main/java/Backend/socket/global/error/dto/ErrorBaseResponse.java create mode 100644 socket/src/main/java/Backend/socket/global/error/handler/MessageErrorHandler.java create mode 100644 socket/src/main/java/Backend/socket/global/error/httpException/BusinessException.java create mode 100644 socket/src/main/java/Backend/socket/global/error/httpException/InternalServerException.java create mode 100644 socket/src/main/java/Backend/socket/global/error/httpException/InvalidValueException.java create mode 100644 socket/src/main/java/Backend/socket/global/error/httpException/UnauthorizedException.java create mode 100644 socket/src/main/java/Backend/socket/global/error/socketException/BusinessException.java create mode 100644 socket/src/main/java/Backend/socket/global/error/socketException/EntityNotFoundException.java create mode 100644 socket/src/main/java/Backend/socket/global/error/socketException/UnauthorizedException.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/AgentWebSocketHandlerDecoratorFactory.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/AwsS3Config.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/FCMConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/MongoDbConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/RedisConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/SocketConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/SocketSecurityConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/SubProtocolWebSocketHandlerInterceptor.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/WebConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/ExceptionHandlerFilter.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/IgnorePathConsts.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationEntryPoint.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationFilter.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/SecurityConfig.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/UserAuthentication.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/UserId.java create mode 100644 socket/src/main/java/Backend/socket/infra/config/auth/UserIdArgumentResolver.java create mode 100644 socket/src/main/java/Backend/socket/infra/external/AwsService.java create mode 100644 socket/src/main/java/Backend/socket/infra/external/fcm/MessageTemplate.java create mode 100644 socket/src/main/java/Backend/socket/infra/external/fcm/repository/FCMTokenRepository.java create mode 100644 socket/src/main/java/Backend/socket/infra/external/fcm/service/PushNotificationService.java create mode 100644 socket/src/main/java/Backend/socket/infra/external/fcm/service/dto/NotificationDto.java create mode 100644 socket/src/test/java/Backend/socket/SocketApplicationTests.java diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..d0f2215 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +이슈 제목 예시(이슈 생성시 삭제) +--- +| 태그 | 제목 | +| --- |-------------------------------------------------------------------------| +| feat | 새로운 기능 구현
ex. [feat]:Main #11 구글 로그인 API 기능 구현 | +| fix | 코드 오류 수정
ex. [fix]:Main #10 회원가입 비즈니스 로직 오류 수정 | +| del | 쓸모없는 코드 삭제
ex. [del]:Main #12 불필요한 import 제거 | +| docs | README나 wiki 등의 문서 개정
ex. [docs]:global #14 리드미 수정 | +| refactor | 내부 로직은 변경 하지 않고 기존의 코드를 개선하는 리팩토링
ex. [refactor]:Global #15 코드 로직 개선 | +| chore | 의존성 추가, yml 추가와 수정, 패키지 구조 변경, 파일 이동
ex. [chore]:Socket #21 yml 수정 | +| test | 테스트 코드 작성, 수정
ex. [test]:Global #20 로그인 API 테스트 코드 작성 | + +--- + +### 📝 Description + +- 구현할 내용 1 +- 구현할 내용 2 + +--- + +### 📝 Todo + +- [ ] : 구현할 내용 1 +- [ ] : 구현할 내용 2 diff --git "a/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" "b/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" new file mode 100644 index 0000000..b90ed06 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\354\235\264\354\212\210-\355\205\234\355\224\214\353\246\277.md" @@ -0,0 +1,34 @@ +--- +name: 이슈 템플릿 +about: 이슈 템플릿 +title: '' +labels: '' +assignees: '' + +--- + +이슈 제목 예시(이슈 생성시 삭제) +--- +| 태그 | 제목 | +| --- |-------------------------------------------------------------------------| +| feat | 새로운 기능 구현
ex. [feat]:Main 구글 로그인 API 기능 구현 | +| fix | 코드 오류 수정
ex. [fix]:Main 회원가입 비즈니스 로직 오류 수정 | +| del | 쓸모없는 코드 삭제
ex. [del]:Main 불필요한 import 제거 | +| docs | README나 wiki 등의 문서 개정
ex. [docs]:global 리드미 수정 | +| refactor | 내부 로직은 변경 하지 않고 기존의 코드를 개선하는 리팩토링
ex. [refactor]:Global 코드 로직 개선 | +| chore | 의존성 추가, yml 추가와 수정, 패키지 구조 변경, 파일 이동
ex. [chore]:Socket yml 수정 | +| test | 테스트 코드 작성, 수정
ex. [test]:Global 로그인 API 테스트 코드 작성 | + +--- + +### 📝 Description + +- 구현할 내용 1 +- 구현할 내용 2 + +--- + +### 📝 Todo + +- [ ] : 구현할 내용 1 +- [ ] : 구현할 내용 2 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7316fa7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,38 @@ +제목 예시: [Main]: #27 엔티티 수정 + +### ✅ PR 유형 +어떤 변경 사항이 있었나요? + +- [ ] 새로운 기능 추가 +- [ ] 버그 수정 +- [ ] 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경) +- [ ] 코드 리팩토링 +- [ ] 주석 추가 및 수정 +- [ ] 문서 수정 +- [ ] 빌드 부분 혹은 패키지 매니저 수정 +- [ ] 파일 혹은 폴더명 수정 +- [ ] 파일 혹은 폴더 삭제 + +--- + +### 📝 작업 내용 +이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +- 작업한 내용 1 +- 작업한 내용 2 + +--- + +### ✏️ 이슈닫기(선택 사항) +해결한 이슈 닫기 + +ex) +closed #(이슈번호) + +--- + +### 🎸 기타 사항 or 추가 코멘트 + + + + diff --git a/.github/workflows/main-service-ci.yml b/.github/workflows/main-service-ci.yml new file mode 100644 index 0000000..d3dfb3b --- /dev/null +++ b/.github/workflows/main-service-ci.yml @@ -0,0 +1,45 @@ +name: Main Service CI + +on: + pull_request: + branches: [ "develop" ] + +jobs: + check-skip: + name: Check ot skip CI + runs-on: ubuntu-latest + if: ${{ contains(github.event.head_commit.message, 'Main') || contains(github.event.head_commit.message, 'Global') }} + steps: + - run: echo "${{ github.event.head_commit.message }}" + + build: + runs-on: ubuntu-latest + needs: check-skip + steps: + - uses: actions/checkout@v3 + - name: 🍀 JDK 17 설정 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: 🍀 application.yml 설정 + run: | + cd ./MainService + cd ./src/main + mkdir resources + cd ./resources + touch ./application.yml + echo "$APPLICATION_DEV" > ./application.yml + env: + APPLICATION_MAIN: ${{ secrets.APPLICATION_DEV }} + + - name: 🍀 gradle build를 위한 권한 설정 + run: | + cd ./Main + chmod +x gradlew + + - name: 🍀 gradle build + run: | + cd ./Main + ./gradlew build -x test \ No newline at end of file diff --git a/.github/workflows/main-service-cicd.yml b/.github/workflows/main-service-cicd.yml new file mode 100644 index 0000000..f1009a7 --- /dev/null +++ b/.github/workflows/main-service-cicd.yml @@ -0,0 +1,87 @@ +name: Main Service CICD + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + check-skip: + name: Check ot skip CI + runs-on: ubuntu-latest + if: ${{ contains(github.event.head_commit.message, 'Main') || contains(github.event.head_commit.message, 'Global') }} + steps: + - run: echo "${{ github.event.head_commit.message }}" + + build: + runs-on: ubuntu-latest + needs: check-skip + steps: + - uses: actions/checkout@v3 + - name: 🍀 JDK 17 설정 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: 🍀 application.yml 설정 + run: | + cd ./Main + cd ./src/main + mkdir resources + cd ./resources + touch ./application.yml + echo "$APPLICATION_DEV" > ./application.yml + env: + APPLICATION_MAIN: ${{ secrets.APPLICATION_DEV }} + + - name: 🍀 gradle build를 위한 권한 설정 + run: | + cd ./Main + chmod +x gradlew + + - name: 🍀 gradle build + run: | + cd ./Main + ./gradlew build -x test + + - name: 🍀 docker image build 후 docker hub에 push + run: | + cd ./Main + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker build -t ${{ secrets.DOCKER_REPOSITORY }}/${{ secrets.MAIN_DOCKER_IMAGE }} . + docker push ${{ secrets.DOCKER_REPOSITORY }}/${{ secrets.DOCKER_IMAGE }} + + - name: 🍀 deploy.sh 파일을 EC2 development server로 전달 + uses: appleboy/scp-action@master + with: + username: ubuntu + host: ${{ secrets.EC2_HOST }} + key: ${{ secrets.EC2_KEY }} + port: ${{ secrets.EC2_PORT }} + source: "./scripts/deploy.sh" + target: "/home/ubuntu/" + + - name: 🍀 docker-compose.yml 파일을 EC2 development server로 전달 + uses: appleboy/scp-action@master + with: + username: ubuntu + host: ${{ secrets.MAIN_EC2_HOST }} + key: ${{ secrets.MAIN_EC2_KEY }} + port: ${{ secrets.MAIN_EC2_PORT }} + source: "./Main/docker-compose.yml" + target: "/home/ubuntu/" + + - name: 🍀 docker hub 에서 pull 후 deploy + uses: appleboy/ssh-action@master + with: + username: ubuntu + host: ${{ secrets.EC2_HOST }} + key: ${{ secrets.EC2_KEY }} + script: | + sudo docker pull ${{ secrets.DOCKER_REPOSITORY }}/${{ secrets.DOCKER_IMAGE }} + chmod 777 ./scripts/deploy.sh + cp ./scripts/deploy.sh ./deploy.sh + ./deploy.sh + docker image prune -f diff --git a/.github/workflows/socket-service-ci.yml b/.github/workflows/socket-service-ci.yml new file mode 100644 index 0000000..577f3f7 --- /dev/null +++ b/.github/workflows/socket-service-ci.yml @@ -0,0 +1,46 @@ +name: Socket Service CI + +on: + pull_request: + branches: [ "develop" ] + +jobs: + check-skip: + name: Check ot skip CI + runs-on: ubuntu-latest + if: ${{ contains(github.event.head_commit.message, 'Socket') || contains(github.event.head_commit.message, 'Global') }} + steps: + - run: echo "${{ github.event.head_commit.message }}" + + build: + runs-on: ubuntu-latest + needs: check-skip + steps: + - uses: actions/checkout@v3 + + - name: 🍀 JDK 17 설정 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: 🍀 application.yml 설정 + run: | + cd ./socket + cd ./src/main + mkdir resources + cd ./resources + touch ./application.yml + echo "$APPLICATION_SOCKET" > ./application.yml + env: + APPLICATION_SOCKET: ${{ secrets.APPLICATION_SOCKET }} + + - name: 🍀 gradle build를 위한 권한 설정 + run: | + cd ./socket + chmod +x gradlew + + - name: 🍀 gradle build + run: | + cd ./socket + ./gradlew build -x test \ No newline at end of file diff --git a/.github/workflows/socket-service-cicd.yml b/.github/workflows/socket-service-cicd.yml new file mode 100644 index 0000000..8474791 --- /dev/null +++ b/.github/workflows/socket-service-cicd.yml @@ -0,0 +1,85 @@ +name: Socket Service CICD + +on: + push: + branches: [ "develop" ] + +jobs: + check-skip: + name: Check ot skip CI + runs-on: ubuntu-latest + if: ${{ contains(github.event.head_commit.message, 'Socket') || contains(github.event.head_commit.message, 'Global') }} + steps: + - run: echo "${{ github.event.head_commit.message }}" + + build: + runs-on: ubuntu-latest + needs: check-skip + steps: + - uses: actions/checkout@v3 + - name: 🍀 JDK 17 설정 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: 🍀 application.yml 설정 + run: | + cd ./socket + cd ./src/main + mkdir resources + cd ./resources + touch ./application.yml + echo "$APPLICATION_SOCKET" > ./application.yml + env: + APPLICATION_SOCKET: ${{ secrets.APPLICATION_SOCKET }} + + - name: 🍀 gradle build를 위한 권한 설정정 + run: | + cd ./socket + chmod +x gradlew + + - name: 🍀 gradle build + run: | + cd ./socket + ./gradlew build -x test + + - name: 🍀 docker image build 후 docker hub에 push + run: | + cd ./socket + docker login -u ${{ secrets.SOCKET_DOCKER_USERNAME }} -p ${{ secrets.SOCKET_DOCKER_PASSWORD }} + docker build -t ${{ secrets.SOCKET_DOCKER_REPOSITORY }}/${{ secrets.SOCKET_DOCKER_IMAGE }} . + docker push ${{ secrets.SOCKET_DOCKER_REPOSITORY }}/${{ secrets.SOCKET_DOCKER_IMAGE }} + + - name: 🍀 deploy.sh 파일을 EC2 development server로 전달 + uses: appleboy/scp-action@master + with: + username: ubuntu + host: ${{ secrets.SOCKET_EC2_HOST }} + key: ${{ secrets.SOCKET_EC2_KEY }} + port: ${{ secrets.SOCKET_EC2_PORT }} + source: "./scripts/deploy.sh" + target: "/home/ubuntu/" + + - name: 🍀 docker-compose.yml 파일을 EC2 development server로 전달 + uses: appleboy/scp-action@master + with: + username: ubuntu + host: ${{ secrets.SOCKET_EC2_HOST }} + key: ${{ secrets.SOCKET_EC2_KEY }} + port: ${{ secrets.SOCKET_EC2_PORT }} + source: "./socket/docker-compose.yml" + target: "/home/ubuntu/" + + - name: 🍀 docker hub 에서 pull 후 deploy + uses: appleboy/ssh-action@master + with: + username: ubuntu + host: ${{ secrets.SOCKET_EC2_HOST }} + key: ${{ secrets.SOCKET_EC2_KEY }} + script: | + sudo docker pull ${{ secrets.SOCKET_DOCKER_REPOSITORY }}/${{ secrets.SOCKET_DOCKER_IMAGE }} + chmod 777 ./scripts/deploy.sh + cp ./scripts/deploy.sh ./deploy.sh + ./deploy.sh + docker image prune -f \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..70f212e --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9de7d72 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git "a/.idea/\354\213\261\355\201\254.iml" "b/.idea/\354\213\261\355\201\254.iml" new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ "b/.idea/\354\213\261\355\201\254.iml" @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Main/.gitignore b/Main/.gitignore new file mode 100644 index 0000000..7a5803f --- /dev/null +++ b/Main/.gitignore @@ -0,0 +1,42 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Yml ### +**/src/main/resources/application.yml +### firebase ### +/src/main/resources/firebase/ \ No newline at end of file diff --git a/Main/Dockerfile b/Main/Dockerfile new file mode 100644 index 0000000..a8f336c --- /dev/null +++ b/Main/Dockerfile @@ -0,0 +1,3 @@ +FROM openjdk:17 +COPY build/libs/backendH-0.0.1-SNAPSHOT.jar app.jar +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/Main/build.gradle b/Main/build.gradle new file mode 100644 index 0000000..8d6ad3c --- /dev/null +++ b/Main/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.5' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'com.kusitms29' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/Main/docker-compose.yml b/Main/docker-compose.yml new file mode 100644 index 0000000..926dce5 --- /dev/null +++ b/Main/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' +services: + blue: + container_name: blue + image: kusitms29h/kusitms29h + expose: + - 8080 + ports: + - 8081:8080 + environment: + - TZ=Asia/Seoul + green: + container_name: green + image: kusitms29h/kusitms29h + expose: + - 8080 + ports: + - 8082:8080 + environment: + - TZ=Asia/Seoul \ No newline at end of file diff --git a/Main/gradle/wrapper/gradle-wrapper.jar b/Main/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Main/gradlew.bat b/Main/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/Main/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Main/settings.gradle b/Main/settings.gradle new file mode 100644 index 0000000..b5f9240 --- /dev/null +++ b/Main/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'backendH' diff --git a/Main/src/main/java/com/kusitms29/backendH/BackendHApplication.java b/Main/src/main/java/com/kusitms29/backendH/BackendHApplication.java new file mode 100644 index 0000000..21d6cac --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/BackendHApplication.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BackendHApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendHApplication.class, args); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/TestController.java b/Main/src/main/java/com/kusitms29/backendH/TestController.java new file mode 100644 index 0000000..242bd56 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/TestController.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + @GetMapping("/test1") + public String test() { + return "hello!"; + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/controller/CommunityController.java b/Main/src/main/java/com/kusitms29/backendH/api/community/controller/CommunityController.java new file mode 100644 index 0000000..d97887c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/controller/CommunityController.java @@ -0,0 +1,144 @@ +package com.kusitms29.backendH.api.community.controller; + +import com.kusitms29.backendH.api.community.service.*; +import com.kusitms29.backendH.api.community.service.dto.request.CommentCreateRequestDto; +import com.kusitms29.backendH.api.community.service.dto.request.PostCreateRequestDto; +import com.kusitms29.backendH.api.community.service.dto.response.*; +import com.kusitms29.backendH.api.user.service.UserService; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.config.auth.UserId; +import com.kusitms29.backendH.infra.external.clova.papago.PapagoService; +import com.kusitms29.backendH.infra.external.clova.papago.detection.LanguageDetectionRequest; +import com.kusitms29.backendH.infra.external.clova.papago.detection.LanguageDetectionResponse; +import com.kusitms29.backendH.infra.external.clova.papago.translation.TextTranslationRequest; +import com.kusitms29.backendH.infra.external.clova.papago.translation.TextTranslationResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/community") +@RestController +public class CommunityController { + private final UserService userService; + private final PostService postService; + private final PostSearchService postSearchService; + private final PostLikeService postLikeService; + private final CommentService commentService; + private final CommentLikeService commentLikeService; + private final ReplyService replyService; + private final ReplyLikeService replyLikeService; + private final PapagoService papagoService; + + @GetMapping("/banner-image") + public ResponseEntity> getLoginUserImage(@UserId Long userId) { + BannerImageResponseDto responseDto = userService.getLoginUserImage(userId); + return SuccessResponse.ok(responseDto); + } + + @GetMapping("/post") + public ResponseEntity> getPostByPostType(@UserId Long userId, @RequestParam String postType) { + List responseDto = postService.getPostByPostType(userId, postType); + return SuccessResponse.ok(responseDto); + } + + @GetMapping("/post/{postId}") + public ResponseEntity> getDetailPost(@UserId Long userId, @PathVariable Long postId) { + PostDetailResponseDto responseDto = postService.getDetailPost(userId, postId); + return SuccessResponse.ok(responseDto); + } + + @PostMapping("/post") + public ResponseEntity> createPost(@UserId Long userId, + @RequestPart(required = false) List images, + @RequestPart PostCreateRequestDto requestDto) { + PostCreateResponseDto responseDto = postService.createPost(userId, images, requestDto); + return SuccessResponse.ok(responseDto); + } + + @GetMapping("/post/search") + public ResponseEntity> searchPost(@UserId Long userId, @RequestParam String keyword) { + List responseDtos = postSearchService.searchPosts(userId, keyword); + return SuccessResponse.ok(responseDtos); + } + @PostMapping("/post/like/{postId}") + public ResponseEntity> createPostLike(@UserId Long userId, @PathVariable Long postId) { + postLikeService.createPostLike(userId, postId); + return SuccessResponse.ok(true); + } + @DeleteMapping("/post/like/{postId}") + public ResponseEntity> deletePostLike(@UserId Long userId, @PathVariable Long postId) { + postLikeService.deletePostLike(userId, postId); + return SuccessResponse.ok(true); + } + + @GetMapping("/comment/{postId}") + public ResponseEntity> getCommentsInPost(@UserId Long userId, @PathVariable Long postId) { + List comments = commentService.getCommentsInPost(userId, postId); + return SuccessResponse.ok(comments); + } + @PostMapping("/comment/{postId}") + public ResponseEntity> createComment(@UserId Long userId, @PathVariable Long postId, + @RequestBody CommentCreateRequestDto content) { + CommentCreateResponseDto commentCreateResponseDto = commentService.createComment(userId, postId, content.getContent()); + return SuccessResponse.ok(commentCreateResponseDto); + } + @PostMapping("/comment/like/{commentId}") + public ResponseEntity> createCommentLike(@UserId Long userId, @PathVariable Long commentId) { + commentLikeService.createCommentLike(userId, commentId); + return SuccessResponse.ok(true); + } + + @DeleteMapping("/comment/like/{commentId}") + public ResponseEntity> deleteCommentLike(@UserId Long userId, @PathVariable Long commentId) { + commentLikeService.deleteCommentLike(userId, commentId); + return SuccessResponse.ok(true); + } + + @PostMapping("/comment/report/{commentId}") + public ResponseEntity> reportComment(@UserId Long userId, @PathVariable Long commentId) { + int reportedCount = commentService.reportComment(userId, commentId); + return SuccessResponse.ok(true); + } + + @PostMapping("/reply/{commentId}") + public ResponseEntity> createReply(@UserId Long userId, @PathVariable Long commentId, + @RequestBody CommentCreateRequestDto requestDto) { + ReplyCreateResponseDto responseDto = replyService.createReply(userId, commentId, requestDto.getContent()); + return SuccessResponse.ok(responseDto); + } + + @PostMapping("/reply/like/{replyId}") + public ResponseEntity> createReplyLike(@UserId Long userId, @PathVariable Long replyId) { + replyLikeService.createReplyLike(userId, replyId); + return SuccessResponse.ok(true); + } + + @DeleteMapping("/reply/like/{replyId}") + public ResponseEntity> deleteReplyLike(@UserId Long userId, @PathVariable Long replyId) { + replyLikeService.deleteReplyLike(userId, replyId); + return SuccessResponse.ok(true); + } + + @PostMapping("/reply/report/{replyId}") + public ResponseEntity> reportReply(@UserId Long userId, @PathVariable Long replyId) { + int reportedCount = replyService.reportReply(userId, replyId); + return SuccessResponse.ok(true); + } + + @PostMapping("/translate") + public ResponseEntity> translateText(@RequestBody TextTranslationRequest requestDto) { + TextTranslationResponse responseDto = papagoService.translateText(requestDto); + return SuccessResponse.ok(responseDto.getMessage().getResult()); + } + + @PostMapping("/check-language") + public ResponseEntity> whatLanguageIsIt(@RequestBody LanguageDetectionRequest requestDto) { + LanguageDetectionResponse responseDto = papagoService.checkLanguage(requestDto); + return SuccessResponse.ok(responseDto); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentLikeService.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentLikeService.java new file mode 100644 index 0000000..13d8e18 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentLikeService.java @@ -0,0 +1,50 @@ +package com.kusitms29.backendH.api.community.service; + +import com.kusitms29.backendH.domain.comment.entity.Comment; +import com.kusitms29.backendH.domain.comment.entity.CommentLike; +import com.kusitms29.backendH.domain.comment.service.CommentLikeModifier; +import com.kusitms29.backendH.domain.comment.service.CommentLikeReader; +import com.kusitms29.backendH.domain.comment.service.CommentReader; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.error.exception.ConflictException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentLikeService { + private final UserReader userReader; + private final CommentReader commentReader; + private final CommentLikeReader commentLikeReader; + private final CommentLikeModifier commentLikeModifier; + + public void createCommentLike(Long userId, Long commentId) { + User user = userReader.findByUserId(userId); + Comment comment = commentReader.findById(commentId); + + if(commentLikeReader.existsByCommentIdAndUserId(commentId, userId)) { + throw new ConflictException(DUPLICATE_COMMENT_LIKE); + } + + commentLikeModifier.save + (CommentLike.builder() + .user(user) + .comment(comment) + .build()); + } + + public void deleteCommentLike(Long userId, Long commentId) { + User user = userReader.findByUserId(userId); + Comment comment = commentReader.findById(commentId); + CommentLike commentLike = commentLikeReader.findByCommentIdAndUserId(commentId, userId); + commentLikeModifier.delete(commentLike); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentService.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentService.java new file mode 100644 index 0000000..49a93fb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/CommentService.java @@ -0,0 +1,145 @@ +package com.kusitms29.backendH.api.community.service; + +import com.kusitms29.backendH.api.community.service.dto.response.CommentCreateResponseDto; +import com.kusitms29.backendH.api.community.service.dto.response.CommentResponseDto; +import com.kusitms29.backendH.api.community.service.dto.response.ReplyResponseDto; +import com.kusitms29.backendH.domain.comment.entity.Comment; +import com.kusitms29.backendH.domain.comment.entity.Reply; +import com.kusitms29.backendH.domain.comment.service.*; +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.service.PostReader; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.common.TimeCalculator; +import com.kusitms29.backendH.global.error.exception.NotAllowedException; +import com.kusitms29.backendH.infra.external.fcm.service.PushNotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Hibernate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentService { + private final CommentReader commentReader; + private final CommentLikeReader commentLikeReader; + private final CommentLikeManager commentLikeManager; + private final CommentLikeModifier commentLikeModifier; + private final PostReader postReader; + private final UserReader userReader; + private final CommentModifier commentModifier; + private final ReplyReader replyReader; + private final ReplyModifier replyModifier; + private final ReplyLikeReader replyLikeReader; + private final ReplyLikeManager replyLikeManager; + private final ReplyLikeModifier replyLikeModifier; + private final PushNotificationService pushNotificationService; + + public List getCommentsInPost(Long userId, Long postId) { + List comments = commentReader.findByPostId(postId); + return comments.stream() + .map(comment -> mapToCommentResponseDto(comment, userId)) + .collect(Collectors.toList()); + } + + private CommentResponseDto mapToCommentResponseDto(Comment comment, Long userId) { + User user = userReader.findByUserId(userId); + int commentLikeCnt = commentLikeManager.countByCommentId(comment.getId()); + + boolean isLikedByUser = commentLikeReader.findByCommentId(comment.getId()) + .stream() + .anyMatch(commentLike -> commentLike.getUser().getId() == userId); + + boolean isCommentedByUser = comment.getUser().getId() == userId; + + List replyList = replyReader.findByCommentId(comment.getId()); + List replyResponseDto = replyList.stream() + .map(reply -> + ReplyResponseDto.builder() + .replyId(reply.getId()) + .writerImage(reply.getUser().getProfile()) + .writerName(reply.getUser().getUserName()) + .createdDate(TimeCalculator.calculateTimeDifference(reply.getCreatedAt())) + .content(reply.getContent()) + .likeCnt(replyLikeManager.countByReplyId(reply.getId())) + .isLikedByUser(replyLikeReader.findByReplyId(reply.getId()) + .stream().anyMatch(replyLike -> replyLike.getUser().getId() == userId)) + .isRepliedByUser(reply.getUser().getId() == userId) + .reportedCnt(reply.getReported()) + .build()) + .collect(Collectors.toList()); + + return CommentResponseDto.of( + comment.getId(), + comment.getUser().getProfile(), + comment.getUser().getUserName(), + comment.getCreatedAt(), + comment.getContent(), + commentLikeCnt, + isLikedByUser, + comment.getReported(), + isCommentedByUser, + replyResponseDto + ); + } + + public CommentCreateResponseDto createComment(Long userId, Long postId, String content) { + Post post = postReader.findById(postId); + User writer = userReader.findByUserId(userId); + + if(content.length() > 30) { + throw new NotAllowedException(TOO_LONG_COMMENT_NOT_ALLOWED); + } + + Comment newComment = commentModifier.save + (Comment.builder() + .post(post) + .user(writer) + .content(content) + .build()); + + pushNotificationService.sendCommentNotification(postId, newComment); + + return CommentCreateResponseDto.of( + newComment.getId(), + newComment.getUser().getProfile(), + newComment.getUser().getUserName(), + newComment.getCreatedAt(), + newComment.getContent(), + newComment.getUser().getId() == userId + ); + } + + public int reportComment(Long userId, Long commentId) { + Comment comment = commentReader.findById(commentId); + User user = userReader.findByUserId(userId); + + if(comment.getReported() >= 2) { + List replyList = replyReader.findByCommentId(commentId); + for(Reply reply : replyList) { + //대댓글좋아요 삭제 + replyLikeModifier.deleteAllByReplyId(reply.getId()); + } + //대댓글 삭제 + replyModifier.deleteAllByCommentId(commentId); + //댓글 좋아요 삭제 + commentLikeModifier.deleteAllByCommentId(commentId); + commentModifier.delete(comment); + return 3; + } + + commentModifier.increaseReportedCount(commentId); + return comment.getReported(); + } + + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostLikeService.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostLikeService.java new file mode 100644 index 0000000..ab123e5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostLikeService.java @@ -0,0 +1,52 @@ +package com.kusitms29.backendH.api.community.service; + +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.entity.PostLike; +import com.kusitms29.backendH.domain.post.service.*; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.error.exception.ConflictException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostLikeService { + private final UserReader userReader; + private final PostReader postReader; + private final PostLikeManager postLikeManager; + private final PostLikeAppender postLikeAppender; + private final PostLikeReader postLikeReader; + private final PostLikeModifier postLikeModifier; + + public void createPostLike(Long userId, Long postId) { + User user = userReader.findByUserId(userId); + Post post = postReader.findById(postId); + + if(postLikeManager.existsByPostIdAndUserId(postId, userId)) { + throw new ConflictException(DUPLICATE_POST_LIKE); + } + + postLikeAppender.save( + PostLike.builder() + .user(user) + .post(post) + .build() + ); + } + + public void deletePostLike(Long userId, Long postId) { + User user = userReader.findByUserId(userId); + Post post = postReader.findById(postId); + PostLike postLike = postLikeReader.findByPostIdAndUserId(postId, userId); + + + postLikeModifier.delete(postLike); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostSearchService.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostSearchService.java new file mode 100644 index 0000000..d0e43e1 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostSearchService.java @@ -0,0 +1,63 @@ +package com.kusitms29.backendH.api.community.service; + + +import com.kusitms29.backendH.api.community.service.dto.request.PostCalculateDto; +import com.kusitms29.backendH.api.community.service.dto.response.PostSearchResponseDto; +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.entity.PostImage; +import com.kusitms29.backendH.domain.post.service.PostImageReader; +import com.kusitms29.backendH.domain.post.service.PostReader; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostSearchService { + private final UserReader userReader; + private final PostReader postReader; + private final PostImageReader postImageReader; + private final PostService postService; + + public List searchPosts(Long userId, String keyword) { + User user = userReader.findByUserId(userId); + + if(keyword == null || keyword.isEmpty()) { + return new ArrayList<>(); + } + + List posts = postReader.searchByTitleOrContent(keyword); + return posts.stream() + .map(post -> { + PostCalculateDto postCalculateDto = postService.calculatePostDetail(post, user.getId()); + PostImage postImage = postImageReader.findByPostIdAndIsRepresentative(post.getId(), true); + + return PostSearchResponseDto.of( + post.getId(), + post.getPostType().getStringPostType(), + post.getUser().getProfile(), + post.getUser().getUserName(), + post.getCreatedAt(), + post.getTitle(), + post.getContent(), + (postImage != null) ? postImage.getImage_url() : null, + postCalculateDto.getLikeCount(), + postCalculateDto.isLikedByUser(), + postCalculateDto.getCommentCount(), + postCalculateDto.isPostedByUser() + ); + }) + .collect(Collectors.toList()); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostService.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostService.java new file mode 100644 index 0000000..608ea51 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/PostService.java @@ -0,0 +1,160 @@ +package com.kusitms29.backendH.api.community.service; + +import com.kusitms29.backendH.api.community.service.dto.request.PostCalculateDto; +import com.kusitms29.backendH.api.community.service.dto.request.PostCreateRequestDto; +import com.kusitms29.backendH.api.community.service.dto.response.PostCreateResponseDto; +import com.kusitms29.backendH.api.community.service.dto.response.PostDetailResponseDto; +import com.kusitms29.backendH.api.community.service.dto.response.PostResponseDto; +import com.kusitms29.backendH.domain.comment.service.CommentManager; +import com.kusitms29.backendH.domain.comment.service.CommentReader; +import com.kusitms29.backendH.domain.comment.service.ReplyManager; +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.entity.PostImage; +import com.kusitms29.backendH.domain.post.entity.PostType; +import com.kusitms29.backendH.domain.post.service.*; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.error.exception.NotAllowedException; +import com.kusitms29.backendH.infra.config.AwsS3Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostService { + private final AwsS3Service awsS3Service; + private final PostReader postReader; + private final PostLikeManager postLikeManager; + private final CommentManager commentManager; + private final CommentReader commentReader; + private final ReplyManager replyManager; + private final PostImageReader postImageReader; + private final UserReader userReader; + private final PostAppender postAppender; + private final PostImageAppender postImageAppender; + + public List getPostByPostType(Long userId, String postType) { + PostType enumPostType = PostType.getEnumPostTypeFromStringPostType(postType); + List posts = postReader.findByPostType(enumPostType); + + posts.sort(Comparator.comparing(Post :: getCreatedAt).reversed()); + + return posts.stream() + .map(post -> mapToPostResponseDto(post, userId)) + .collect(Collectors.toList()); + } + + private PostResponseDto mapToPostResponseDto(Post post, Long userId) { + PostCalculateDto postCalculateDto = calculatePostDetail(post, userId); + PostImage postImage = postImageReader.findByPostIdAndIsRepresentative(post.getId(), true); + + return PostResponseDto.of( + post.getId(), + post.getPostType().getStringPostType(), + post.getUser().getProfile(), + post.getUser().getUserName(), + post.getCreatedAt(), + post.getTitle(), + post.getContent(), + (postImage != null) ? postImage.getImage_url() : null, + postCalculateDto.getLikeCount(), + postCalculateDto.isLikedByUser(), + postCalculateDto.getCommentCount(), + postCalculateDto.isPostedByUser() + ); + } + + public PostDetailResponseDto getDetailPost(Long userId, Long postId) { + Post post = postReader.findById(postId); + PostCalculateDto postCalculateDto = calculatePostDetail(post, userId); + + List imageUrls = postImageReader.findByPostId(post.getId()) + .stream() + .map(PostImage::getImage_url) + .collect(Collectors.toList()); + + return PostDetailResponseDto.of( + post.getPostType().getStringPostType(), + post.getUser().getProfile(), + post.getUser().getUserName(), + post.getCreatedAt(), + post.getTitle(), + post.getContent(), + postCalculateDto.getLikeCount(), + postCalculateDto.isLikedByUser(), + postCalculateDto.getCommentCount(), + postCalculateDto.isPostedByUser(), + imageUrls + ); + } + + public PostCalculateDto calculatePostDetail(Post post, Long userId) { + int likeCount = postLikeManager.countByPostId(post.getId()); + boolean isLikedByUser = postLikeManager.existsByPostIdAndUserId(post.getId(), userId); + + int totalCommentCount = commentManager.countByPostId(post.getId()); + int replyCount = commentReader.findByPostId(post.getId()).stream() + .mapToInt(comment -> replyManager.countByCommentId(comment.getId())) + .sum(); + totalCommentCount += replyCount; + + boolean isPostedByUser = post.getUser().getId() == userId; + return new PostCalculateDto(likeCount, isLikedByUser, totalCommentCount, isPostedByUser); + } + + public PostCreateResponseDto createPost(Long userId, List images, PostCreateRequestDto requestDto) { + User writer = userReader.findByUserId(userId); + PostType postType = PostType.getEnumPostTypeFromStringPostType(requestDto.getPostType()); + + String title = requestDto.getTitle(); + String content = requestDto.getContent(); + if(title.length() > 30) { + throw new NotAllowedException(TOO_LONG_TITLE_NOT_ALLOWED); + } + if(content.length() > 300) { + throw new NotAllowedException(TOO_LONG_CONTENT_NOT_ALLOWED); + } + if(images != null && !images.isEmpty() && images.size() > 5) { + throw new NotAllowedException(TOO_MANY_IMAGES_NOT_ALLOWED); + } + + Post newPost = postAppender.save + (Post.builder() + .user(writer) + .postType(postType) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .build()); + + List imageUrls = null; + if(images != null && !images.isEmpty()) { + imageUrls = awsS3Service.uploadImages(images); + for(int i=0; i= 2) { + replyLikeModifier.deleteAllByReplyId(replyId); + replyModifier.delete(reply); + return 3; + } + + replyModifier.increaseReportedCount(replyId); + return reply.getReported(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/CommentCreateRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/CommentCreateRequestDto.java new file mode 100644 index 0000000..8fdb7ec --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/CommentCreateRequestDto.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.api.community.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentCreateRequestDto { + private String content; +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCalculateDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCalculateDto.java new file mode 100644 index 0000000..745f443 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCalculateDto.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.api.community.service.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class PostCalculateDto { + private int likeCount; + private boolean isLikedByUser; + private int commentCount; + private boolean isPostedByUser; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCreateRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCreateRequestDto.java new file mode 100644 index 0000000..3d7eb4e --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/request/PostCreateRequestDto.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.api.community.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class PostCreateRequestDto { + private String postType; + private String title; + private String content; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/BannerImageResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/BannerImageResponseDto.java new file mode 100644 index 0000000..952995a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/BannerImageResponseDto.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class BannerImageResponseDto { + String image; + + public static BannerImageResponseDto of(String image) { + return BannerImageResponseDto.builder() + .image(image) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentCreateResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentCreateResponseDto.java new file mode 100644 index 0000000..dd80b87 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentCreateResponseDto.java @@ -0,0 +1,31 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class CommentCreateResponseDto { + private Long commentId; + private String writerImage; + private String writerName; + private String createdDate; + private String content; + private boolean isCommentedByUser; + + public static CommentCreateResponseDto of(Long commentId, String writerImage, String writerName, + LocalDateTime createdDate, String content, boolean isCommentedByUser) { + + return CommentCreateResponseDto.builder() + .commentId(commentId) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .content(content) + .isCommentedByUser(isCommentedByUser) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentResponseDto.java new file mode 100644 index 0000000..da02964 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/CommentResponseDto.java @@ -0,0 +1,42 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@Getter +public class CommentResponseDto { + private Long commentId; + private String writerImage; + private String writerName; + private String createdDate; + private String content; + private int likeCnt; + private boolean isLikedByUser; + private int reportedCnt; + private boolean isCommentedByUser; + private List replyList; + + public static CommentResponseDto of(Long commentId, String writerImage, String writerName, + LocalDateTime createdDate, String content, int likeCnt, + boolean isLikedByUser, int reportedCnt, Boolean isCommentedByUser, + List replyList) { + + return CommentResponseDto.builder() + .commentId(commentId) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .content(content) + .likeCnt(likeCnt) + .isLikedByUser(isLikedByUser) + .reportedCnt(reportedCnt) + .isCommentedByUser(isCommentedByUser) + .replyList(replyList) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostCreateResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostCreateResponseDto.java new file mode 100644 index 0000000..68856b5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostCreateResponseDto.java @@ -0,0 +1,25 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class PostCreateResponseDto { + private String postType; + private String title; + private String content; + private List imageUrls; + + public static PostCreateResponseDto of(String postType, String title, + String content, List imageUrls) { + return PostCreateResponseDto.builder() + .postType(postType) + .title(title) + .content(content) + .imageUrls(imageUrls) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostDetailResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostDetailResponseDto.java new file mode 100644 index 0000000..c5b7751 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostDetailResponseDto.java @@ -0,0 +1,45 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@Getter +public class PostDetailResponseDto { + private String postType; + private String writerImage; + private String writerName; + private String createdDate; + private String title; + private String content; + private int likeCnt; + private boolean isLikedByUser; + private int commentCnt; + private boolean isPostedByUser; + private List imageUrls; + + public static PostDetailResponseDto of(String postType, String writerImage, String writerName, + LocalDateTime createdDate, String title, String content, + int likeCnt, boolean isLikedByUser, int commentCnt, boolean isPostedByUser, + List imageUrls) { + return PostDetailResponseDto.builder() + .postType(postType) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .title(title) + .content(content) + .likeCnt(likeCnt) + .isLikedByUser(isLikedByUser) + .commentCnt(commentCnt) + .isPostedByUser(isPostedByUser) + .imageUrls(imageUrls) + .build(); + } + + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostResponseDto.java new file mode 100644 index 0000000..b02a145 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostResponseDto.java @@ -0,0 +1,44 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.Duration; +import java.time.LocalDateTime; + +@Builder +@Getter +public class PostResponseDto { + private long postId; + private String postType; + private String writerImage; + private String writerName; + private String createdDate; + private String title; + private String content; + private String representativeImage; + private int likeCnt; + private boolean isLikedByUser; + private int commentCnt; + private boolean isPostedByUser; + + public static PostResponseDto of(Long postId, String postType, String writerImage, String writerName, + LocalDateTime createdDate, String title, String content, String representativeImage, + int likeCnt, boolean isLikedByUser, int commentCnt, boolean isPostedByUser) { + return PostResponseDto.builder() + .postId(postId) + .postType(postType) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .title(title) + .content(content) + .representativeImage(representativeImage) + .likeCnt(likeCnt) + .isLikedByUser(isLikedByUser) + .commentCnt(commentCnt) + .isPostedByUser(isPostedByUser) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostSearchResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostSearchResponseDto.java new file mode 100644 index 0000000..316fcad --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/PostSearchResponseDto.java @@ -0,0 +1,43 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class PostSearchResponseDto { + private Long postId; + private String postType; + private String writerImage; + private String writerName; + private String createdDate; + private String title; + private String content; + private String representativeImage; + private int likeCnt; + private boolean isLikedByUser; + private int commentCnt; + private boolean isPostedByUser; + + public static PostSearchResponseDto of(Long postId, String postType, String writerImage, String writerName, LocalDateTime createdDate, + String title, String content, String representativeImage, + int likeCnt, boolean isLikedByUser, int commentCnt, boolean isPostedByUser) { + return PostSearchResponseDto.builder() + .postId(postId) + .postType(postType) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .title(title) + .content(content) + .representativeImage(representativeImage) + .likeCnt(likeCnt) + .isLikedByUser(isLikedByUser) + .commentCnt(commentCnt) + .isPostedByUser(isPostedByUser) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyCreateResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyCreateResponseDto.java new file mode 100644 index 0000000..472f8d5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyCreateResponseDto.java @@ -0,0 +1,31 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class ReplyCreateResponseDto { + private Long replyId; + private String writerImage; + private String writerName; + private String createdDate; + private String content; + private boolean isRepliedByUser; + + public static ReplyCreateResponseDto of(Long commentId, String writerImage, String writerName, + LocalDateTime createdDate, String content, boolean isRepliedByUser) { + + return ReplyCreateResponseDto.builder() + .replyId(commentId) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .content(content) + .isRepliedByUser(isRepliedByUser) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyResponseDto.java new file mode 100644 index 0000000..bc542de --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/community/service/dto/response/ReplyResponseDto.java @@ -0,0 +1,38 @@ +package com.kusitms29.backendH.api.community.service.dto.response; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class ReplyResponseDto { + private Long replyId; + private String writerImage; + private String writerName; + private String createdDate; + private String content; + private int likeCnt; + private boolean isLikedByUser; + private int reportedCnt; + private boolean isRepliedByUser; + + public static ReplyResponseDto of(Long replyId, String writerImage, String writerName, + LocalDateTime createdDate, String content, + int likeCnt, boolean isLikedByUser, int reportedCnt, boolean isRepliedByUser) { + return ReplyResponseDto.builder() + .replyId(replyId) + .writerImage(writerImage) + .writerName(writerName) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .content(content) + .likeCnt(likeCnt) + .isLikedByUser(isLikedByUser) + .reportedCnt(reportedCnt) + .isRepliedByUser(isRepliedByUser) + .build(); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/notification/controller/NotificationHistoryController.java b/Main/src/main/java/com/kusitms29/backendH/api/notification/controller/NotificationHistoryController.java new file mode 100644 index 0000000..9803f2b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/notification/controller/NotificationHistoryController.java @@ -0,0 +1,29 @@ +package com.kusitms29.backendH.api.notification.controller; + +import com.kusitms29.backendH.api.notification.service.NotificationHistoryService; +import com.kusitms29.backendH.api.notification.service.dto.NotificationHistoryResponseDto; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.config.auth.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/notification") +@RestController +public class NotificationHistoryController { + + private final NotificationHistoryService notificationHistoryService; + + @GetMapping + public ResponseEntity> getNotificationByTopCategory(@UserId Long userId, @RequestParam String topCategory) { + List responseDto = notificationHistoryService.getNotificationByTopCategory(userId, topCategory); + return SuccessResponse.ok(responseDto); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationHistoryService.java b/Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationHistoryService.java new file mode 100644 index 0000000..b38f9a3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationHistoryService.java @@ -0,0 +1,72 @@ +package com.kusitms29.backendH.api.notification.service; + +import com.kusitms29.backendH.api.notification.service.dto.NotificationHistoryResponseDto; +import com.kusitms29.backendH.domain.comment.service.CommentReader; +import com.kusitms29.backendH.domain.notification.entity.NotificationHistory; +import com.kusitms29.backendH.domain.notification.entity.TopCategory; +import com.kusitms29.backendH.domain.notification.service.NotificationHistoryReader; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_NOTIFICATION_TYPE; + +@Service +@RequiredArgsConstructor +public class NotificationHistoryService { + + private final NotificationHistoryReader notificationHistoryReader; + private final CommentReader commentReader; + + public List getNotificationByTopCategory(Long userId, String topCategory) { + TopCategory enumTopCategory = TopCategory.getEnumTopCategoryFromStringTopCategory(topCategory); + List notificationHistories = notificationHistoryReader.findByTopCategoryAndUserId(enumTopCategory, userId); + + notificationHistories.sort(Comparator.comparing(NotificationHistory :: getSentAt).reversed()); + + return notificationHistories.stream() + .map(this::mapToNotificationHistoryResponseDto) + .collect(Collectors.toList()); + } + + private NotificationHistoryResponseDto mapToNotificationHistoryResponseDto(NotificationHistory notificationHistory) { + String detailContent = ""; + + switch (notificationHistory.getNotificationType().name()) { + case "CHAT": + detailContent = notificationHistory.getInfoId2(); + break; + case "CHAT_ROOM_NOTICE": + detailContent = "지금 바로 채팅방에 입장해서 멤버들과 대화를 나눠보세요!"; + break; + case "COMMENT": + Long infoId2 = ((notificationHistory.getInfoId2() != null)&&(!notificationHistory.getInfoId2().isEmpty()) ? Long.parseLong(notificationHistory.getInfoId2()) : null); + detailContent = commentReader.findById(infoId2).getContent(); + break; + case "SYNC_REMINDER": + detailContent = "즐거운 싱크되세요!"; + break; + case "REVIEW": + detailContent = "생생한 리뷰를 남겨보세요"; + break; + default: + throw new InvalidValueException(INVALID_NOTIFICATION_TYPE); + } + + return NotificationHistoryResponseDto.of( + notificationHistory.getInfoId(), + notificationHistory.getTitle(), + notificationHistory.getBody(), + detailContent, + notificationHistory.getSentAt() + ); + } + + + + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationService.java b/Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationService.java new file mode 100644 index 0000000..5157521 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/notification/service/NotificationService.java @@ -0,0 +1,36 @@ +package com.kusitms29.backendH.api.notification.service; + +import com.kusitms29.backendH.domain.notification.entity.NotificationSetting; +import com.kusitms29.backendH.domain.notification.entity.NotificationType; +import com.kusitms29.backendH.domain.notification.repository.NotificationRepository; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.kusitms29.backendH.global.error.ErrorCode.NOTIFICATION_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class NotificationService { + private final NotificationRepository notificationRepository; + @Transactional + public void createNotificationSetting(User user) { + for(NotificationType type : NotificationType.values()) { + NotificationSetting notificationSetting = NotificationSetting.builder() + .user(user) + .notificationType(type) + .build(); + notificationRepository.save(notificationSetting); + } + } + + public void updateSettingActive(User user, NotificationType type) { + NotificationSetting notificationSetting = notificationRepository.findByUserAndNotificationType(user, type) + .orElseThrow(() -> new EntityNotFoundException(NOTIFICATION_NOT_FOUND)); + notificationSetting.setStatus(NotificationSetting.Status.ACTIVE); + notificationRepository.save(notificationSetting); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/notification/service/dto/NotificationHistoryResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/notification/service/dto/NotificationHistoryResponseDto.java new file mode 100644 index 0000000..52e65da --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/notification/service/dto/NotificationHistoryResponseDto.java @@ -0,0 +1,30 @@ +package com.kusitms29.backendH.api.notification.service.dto; + +import com.kusitms29.backendH.global.common.TimeCalculator; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class NotificationHistoryResponseDto { + private String infoId; //커뮤니티: 게시글, 일정: 싱크, 리뷰: 싱크, 채팅: 채팅방, 채팅개설공지: 싱크 + private String title; + private String content; + private String detailContent; + private String createdDate; + + public static NotificationHistoryResponseDto of(String infoId, String title, String content, + String detailContent, LocalDateTime createdDate) { + + return NotificationHistoryResponseDto.builder() + .infoId(infoId) + .title(title) + .content(content) + .detailContent(detailContent) + .createdDate(TimeCalculator.calculateTimeDifference(createdDate)) + .build(); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncDetailController.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncDetailController.java new file mode 100644 index 0000000..ffb87ac --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncDetailController.java @@ -0,0 +1,48 @@ +package com.kusitms29.backendH.api.sync.controller; + +import com.kusitms29.backendH.api.sync.service.SyncDetailService; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncDetailResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncGraphResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncInfoResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncReviewResponseDto; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.config.auth.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RequiredArgsConstructor +@RequestMapping("/api/sync/detail") +@RestController +public class SyncDetailController { + private final SyncDetailService syncDetailService; + @GetMapping + public ResponseEntity> syncDetail(@RequestParam(name = "syncId") Long syncId){ + SyncDetailResponseDto syncDetailResponseDto = syncDetailService.getSyncDetail(syncId); + return SuccessResponse.ok(syncDetailResponseDto); + } + @GetMapping("/{graph}") + public ResponseEntity> syncDetailGraph(@RequestParam(name = "syncId") Long syncId, @PathVariable(name = "graph") String graph){ + SyncGraphResponseDto syncGraphResponseDto = syncDetailService.getSyncDetailGraph(syncId, graph); + return SuccessResponse.ok(syncGraphResponseDto); + } + @GetMapping("/recommend") + public ResponseEntity> getAnotherSync(@RequestParam(name = "syncId") Long syncId,@RequestParam(name = "take", defaultValue = "0") int take){ + List syncInfoResponseDtos = syncDetailService.getSyncListBySameDateAndSameLocation(syncId, take); + return SuccessResponse.ok(syncInfoResponseDtos); + } + @GetMapping("/review") + public ResponseEntity> getSyncReviewList(@RequestParam(name = "syncId") Long syncId, @RequestParam(name = "take",defaultValue = "0") int take){ + List syncReviewResponseDtos = syncDetailService.getSyncReviewList(syncId, take); + return SuccessResponse.ok(syncReviewResponseDtos); + } + @GetMapping("/join") + public ResponseEntity> joinSync(@UserId Long userId, @RequestParam(name = "syncId") Long syncId){ + syncDetailService.joinSync(userId, syncId); + return SuccessResponse.ok("join"); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncMainController.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncMainController.java new file mode 100644 index 0000000..0abbe70 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncMainController.java @@ -0,0 +1,49 @@ +package com.kusitms29.backendH.api.sync.controller; + +import com.kusitms29.backendH.api.sync.service.SyncService; +import com.kusitms29.backendH.api.sync.service.dto.request.SyncInfoRequestDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncAssociateInfoResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncInfoResponseDto; +import com.kusitms29.backendH.domain.user.ip.IpService; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.external.clova.map.GeoLocationService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/sync") +@RestController +public class SyncMainController { + private final SyncService syncManageService; + private final IpService ipService; + private final GeoLocationService geoLocationService; + @GetMapping("/recommend") + public ResponseEntity> recommendSync(@RequestParam(name = "userId") Long userId, HttpServletRequest request) throws NoSuchAlgorithmException, InvalidKeyException, IOException { + String clientIp = ipService.getClientIpAddress(request); +// GeoLocation geoLocation = geoLocationService.getGeoLocation(clientIp); + List syncInfoResponseDtos = syncManageService.recommendSync(userId, clientIp); + return SuccessResponse.ok(syncInfoResponseDtos); + } + @PostMapping("/friend") + public ResponseEntity> friendSync(@RequestBody SyncInfoRequestDto syncInfoRequestDto) { + List syncInfoResponseDtos = syncManageService.friendSync(syncInfoRequestDto); + return SuccessResponse.ok(syncInfoResponseDtos); + } + @PostMapping("/search") + public ResponseEntity> searchSync(@RequestBody SyncInfoRequestDto syncInfoRequestDto) { + List syncInfoResponseDtos = syncManageService.searchSync(syncInfoRequestDto); + return SuccessResponse.ok(syncInfoResponseDtos); + } + @PostMapping("/associate") + public ResponseEntity> associateSync(@RequestBody SyncInfoRequestDto syncInfoRequestDto) { + List syncInfoResponseDtos = syncManageService.associateSync(syncInfoRequestDto); + return SuccessResponse.ok(syncInfoResponseDtos); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncManageController.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncManageController.java new file mode 100644 index 0000000..498835c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/controller/SyncManageController.java @@ -0,0 +1,34 @@ +package com.kusitms29.backendH.api.sync.controller; + +import com.kusitms29.backendH.api.sync.service.SyncService; +import com.kusitms29.backendH.api.sync.service.dto.request.SyncCreateRequestDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncSaveResponseDto; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.config.auth.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/sync") +@RestController +public class SyncManageController { + private final SyncService syncService; + @PostMapping + public ResponseEntity> createSync(@UserId Long userId, + @RequestPart(required = false) MultipartFile image, + @RequestPart SyncCreateRequestDto requestDto) { + SyncSaveResponseDto responseDto = syncService.createSync(userId, image, requestDto); + return SuccessResponse.ok(responseDto); + } + + @GetMapping("/seoul-address") + public ResponseEntity> getSeoulAddresses() { + List responseDto = syncService.getSeoulAddresses(); + return SuccessResponse.ok(responseDto); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncDetailService.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncDetailService.java new file mode 100644 index 0000000..0d5577e --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncDetailService.java @@ -0,0 +1,132 @@ +package com.kusitms29.backendH.api.sync.service; + + +import com.kusitms29.backendH.api.sync.service.dto.response.SyncDetailResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncGraphResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncInfoResponseDto; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncReviewResponseDto; +import com.kusitms29.backendH.domain.chat.service.RoomAppender; +import com.kusitms29.backendH.domain.sync.entity.Participation; +import com.kusitms29.backendH.domain.sync.service.ParticipationManager; +import com.kusitms29.backendH.domain.sync.service.ParticipationReader; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import com.kusitms29.backendH.domain.sync.service.SyncManager; +import com.kusitms29.backendH.domain.sync.service.SyncReader; +import com.kusitms29.backendH.domain.sync.entity.SyncReview; +import com.kusitms29.backendH.domain.sync.service.SyncReviewReader; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.error.ErrorCode; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import com.kusitms29.backendH.global.error.exception.ListException; +import com.kusitms29.backendH.infra.utils.ListUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.kusitms29.backendH.domain.chat.entity.Room.createRoom; +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_SYNC_TYPE; + +@Service +@RequiredArgsConstructor +public class SyncDetailService { + private final SyncReader syncReader; + private final UserReader userReader; + private final ParticipationManager participationManager; + private final SyncManager syncManager; + private final ParticipationReader participationReader; + private final ListUtils listUtils; + private final RoomAppender roomAppender; + public SyncDetailResponseDto getSyncDetail(Long syncId){ + Sync sync = syncReader.findById(syncId); + User user = userReader.findByUserId(sync.getUser().getId()); + int count = participationManager.countParticipationBySyncId(syncId); + Boolean isFull = syncManager.validateJoinRoom(sync,count); + if (sync.getSyncType() == SyncType.ONETIME) { + return SyncDetailResponseDto.oneTimeOf( + sync.getSyncName(), + sync.getImage(), + sync.getSyncType(), + sync.getType(), + sync.getSyncIntro(), + sync.getDate(), + sync.getLocation(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + user.getProfile(), + user.getUserName(), + user.getUniversity(), + sync.getUserIntro(), + isFull + ); + } else if (sync.getSyncType() == SyncType.LONGTIME) { + return SyncDetailResponseDto.longTimeOf( + sync.getSyncName(), + sync.getImage(), + sync.getSyncType(), + sync.getType(), + sync.getSyncIntro(), + sync.getRegularDay(), + sync.getRegularTime(), + sync.getDate(), + sync.getLocation(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + user.getProfile(), + user.getUserName(), + user.getUniversity(), + sync.getUserIntro(), + isFull + ); + } else { + throw new InvalidValueException(INVALID_SYNC_TYPE); + } + } + public SyncGraphResponseDto getSyncDetailGraph(Long syncId, String graph){ + List participations = participationReader.findAllBySyncId(syncId); + SyncGraphResponseDto graphElements = syncManager.createGraphElementList(participations, graph); + return graphElements; + } + public List getSyncListBySameDateAndSameLocation(Long syncId, int take){ + Sync csync = syncReader.findById(syncId); + List syncList= syncReader.findAllByLocationAndDate(csync.getLocation(), csync.getDate()); + List syncInfoResponseDtos = listUtils.getListByTake(syncList.stream() + .filter(sync -> !sync.getId().equals(syncId)) + .map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(), take); + return ListException.throwIfEmpty(syncInfoResponseDtos, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + private final SyncReviewReader syncReviewReader; + public List getSyncReviewList(Long syncId, int take){ + List syncReviews = syncReviewReader.findAllBySyncId(syncId); + List syncReviewResponseDtos = syncReviews.stream(). + map(syncReview -> SyncReviewResponseDto.of( + syncReview.getUser().getProfile(), + syncReview.getUser().getUserName(), + syncReview.getUser().getUniversity(), + syncReview.getContent(), + syncReview.getCreatedAt() + )).toList(); + return listUtils.getListByTake(syncReviewResponseDtos, take); + } + public void joinSync(Long userId, Long syncId){ + Participation.createParticipation(User.from(userId), Sync.from(syncId)); + int count = participationManager.countParticipationBySyncId(syncId); + Boolean isPossible = syncManager.validateCreateRoom(syncReader.findById(syncId),count); + List userList = participationReader.findAllBySyncId(syncId).stream().map(participation -> participation.getUser()).toList(); + roomAppender.createRoom(userList,isPossible,syncId); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncService.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncService.java new file mode 100644 index 0000000..ceb2f5d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/SyncService.java @@ -0,0 +1,233 @@ +package com.kusitms29.backendH.api.sync.service; + + +import com.kusitms29.backendH.api.sync.service.dto.request.SyncCreateRequestDto; +import com.kusitms29.backendH.api.sync.service.dto.request.SyncInfoRequestDto; +import com.kusitms29.backendH.api.sync.service.dto.response.*; +import com.kusitms29.backendH.domain.category.entity.Category; +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.category.service.CategoryReader; +import com.kusitms29.backendH.domain.category.service.UserCategoryManager; +import com.kusitms29.backendH.domain.category.service.UserCategoryReader; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import com.kusitms29.backendH.domain.sync.service.ParticipationManager; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.sync.service.SyncAppender; +import com.kusitms29.backendH.domain.sync.service.SyncReader; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.category.entity.UserCategory; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import com.kusitms29.backendH.global.error.exception.NotAllowedException; +import com.kusitms29.backendH.infra.config.AwsS3Service; +import com.kusitms29.backendH.infra.external.SeoulAddressClient; +import com.kusitms29.backendH.infra.utils.ListUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.kusitms29.backendH.domain.category.entity.Type.getEnumTypeFromStringType; +import static com.kusitms29.backendH.domain.sync.entity.SyncType.FROM_FRIEND; +import static com.kusitms29.backendH.domain.sync.entity.SyncType.getEnumFROMStringSyncType; +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SyncService { + private final SyncReader syncReader; + private final SyncAppender syncAppender; + private final UserReader userReader; + private final UserCategoryReader userCategoryReader; + private final UserCategoryManager userCategoryManager; + private final ParticipationManager participationManager; + private final ListUtils listUtils; + private final AwsS3Service awsS3Service; + private final CategoryReader categoryReader; + private final SeoulAddressClient seoulAddressClient; + + public List recommendSync(Long userId, String clientIp){ + User user = userReader.findByUserId(userId); + List userCategories = userCategoryReader.findAllByUserId(userId); + List types = userCategoryManager.getTypeByUserCategories(userCategories); + List syncList = syncReader.findBySyncTypeWithTypesWithLocation(user.getSyncType(), types, user.getLocation()); + return syncList.stream().map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(); + } + public List friendSync(SyncInfoRequestDto syncInfoRequestDto){ + List syncList = syncReader.findAllBySyncTypeAndType(FROM_FRIEND, getEnumTypeFromStringType(syncInfoRequestDto.type())); + List syncInfoResponseDtos = syncList.stream() + //음 이거보다 위에서 if문써서 하는게 더 가독성 있는듯 +// .filter(sync -> type == null || sync.getType().name().equals(type)) + .map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(); + return listUtils.getListByTake(syncInfoResponseDtos, syncInfoRequestDto.take()); + } + public List associateSync(SyncInfoRequestDto syncInfoRequestDto){ + List syncList = syncReader.findAllByAssociateIsExist(getEnumFROMStringSyncType(syncInfoRequestDto.syncType()), getEnumTypeFromStringType(syncInfoRequestDto.type())); + List syncAssociateInfoResponseDtos = syncList.stream().map( sync -> SyncAssociateInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate(), + sync.getAssociate() + )).toList(); + return listUtils.getListByTake(syncAssociateInfoResponseDtos, syncInfoRequestDto.take()); + } + public List searchSync(SyncInfoRequestDto syncInfoRequestDto){ + List syncList = syncReader.findAllBySyncTypeAndType(getEnumFROMStringSyncType(syncInfoRequestDto.syncType()), getEnumTypeFromStringType(syncInfoRequestDto.type())); + + List syncInfoResponseDtos = syncList.stream().map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(); + return listUtils.getListByTake(syncInfoResponseDtos, syncInfoRequestDto.take()); + } + + public SyncSaveResponseDto createSync(Long userId, MultipartFile file, SyncCreateRequestDto requestDto) { + User user = userReader.findByUserId(userId); + + if(requestDto.getUserIntro().length() > 50) { + throw new NotAllowedException(USER_INTRO_NOT_ALLOWED); + } + if(requestDto.getSyncIntro().length() > 500) { + throw new NotAllowedException(SYNC_INTRO_NOT_ALLOWED); + } + + SyncType enumSyncType = SyncType.getEnumFROMStringSyncType(requestDto.getSyncType()); + + if(requestDto.getSyncName().length() > 15) { + throw new NotAllowedException(SYNC_NAME_NOT_ALLOWED); + } + + String image = awsS3Service.uploadImage(file); + + LocalDateTime oneTimeLocalDateTime = null; + if(requestDto.getDate() != null && !requestDto.getDate().isEmpty()) { + oneTimeLocalDateTime= parseToLocalDateTime(requestDto.getDate()); //2023-04-13 15:30 + } + + String regularDay = null; + if(requestDto.getRegularDay() != null && !requestDto.getRegularDay().isEmpty()) { + regularDay = requestDto.getRegularDay(); + } + + LocalDateTime regularLocalDateTime = null; + if(requestDto.getRoutineDate() != null && !requestDto.getRoutineDate().isEmpty()) { + regularLocalDateTime = parseToLocalDateTime(requestDto.getRoutineDate()); //2023-04-13 15:30 + } + + LocalTime regularLocalTime = null; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + if(requestDto.getRegularTime() != null && !requestDto.getRegularTime().isEmpty()) { //15:30 + regularLocalTime = LocalTime.parse(requestDto.getRegularTime(), formatter); + } + + if(requestDto.getMember_min() < 3) { + throw new NotAllowedException(SYNC_MIN_NOT_ALLOWED); + } + if(requestDto.getMember_max() > 30) { + throw new NotAllowedException(SYNC_MAX_NOT_ALLOWED); + } + + Type enumType = Type.getEnumTypeFromStringType(requestDto.getType()); + + Category detailCategory = categoryReader.findByName(requestDto.getDetailType()); + if(!detailCategory.getType().getStringType().equals(requestDto.getType())) { + throw new InvalidValueException(INVALID_PARENT_CHILD_CATEGORY); + } + + Sync newSync = syncAppender.save( + Sync.createSync( + user, + requestDto.getUserIntro(), + requestDto.getSyncIntro(), + enumSyncType, + requestDto.getSyncName(), + image, + requestDto.getLocation(), + oneTimeLocalDateTime, + regularDay, + regularLocalTime, + regularLocalDateTime, + requestDto.getMember_min(), + requestDto.getMember_max(), + enumType, + requestDto.getDetailType()) + ); + + return SyncSaveResponseDto.of( + newSync.getId(), + newSync.getUserIntro(), + newSync.getSyncIntro(), + newSync.getSyncType(), + newSync.getSyncName(), + newSync.getImage(), + newSync.getLocation(), + newSync.getDate(), + newSync.getRegularDay(), + newSync.getRegularTime(), + newSync.getRoutineDate(), + newSync.getMember_min(), + newSync.getMember_max(), + newSync.getType(), + newSync.getDetailType() + ); + } + + private LocalDateTime parseToLocalDateTime(String date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return LocalDateTime.parse(date, formatter); + } + + public List getSeoulAddresses() { + List nameList = new ArrayList<>(); + SeoulAddressResponse seoulAddressResponse = seoulAddressClient.calloutSeoulAddressAPI(); + + seoulAddressResponse.getRegcodes().forEach(result -> { + String address = result.getName().replace("특별", ""); + nameList.add(address); + }); + + return nameList; + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncCreateRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncCreateRequestDto.java new file mode 100644 index 0000000..4f78107 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncCreateRequestDto.java @@ -0,0 +1,29 @@ +package com.kusitms29.backendH.api.sync.service.dto.request; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class SyncCreateRequestDto { + private String userIntro; + private String syncIntro; + private String syncType; + private String syncName; + private String location; + private String date; //일회성, 내친소 + + private String regularDay; //지속성 + private String regularTime; //지속성 + private String routineDate; //지속성 + + private int member_min; + + private int member_max; + + private String type; //언어교환, 엔터테인먼트, ... + + private String detailType; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncInfoRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncInfoRequestDto.java new file mode 100644 index 0000000..c44f875 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/request/SyncInfoRequestDto.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.api.sync.service.dto.request; + +public record SyncInfoRequestDto( + int take, + String syncType, + String type +) { + public SyncInfoRequestDto { + if (take < 0) { + take = 0; + } + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/GraphElement.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/GraphElement.java new file mode 100644 index 0000000..78dfba4 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/GraphElement.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GraphElement { + private String name; + private int percent; + public static GraphElement of(String name, int percent){ + return GraphElement.builder() + .name(name) + .percent(percent) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SeoulAddressResponse.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SeoulAddressResponse.java new file mode 100644 index 0000000..b22481f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SeoulAddressResponse.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class SeoulAddressResponse { + private List regcodes; + + @Getter + public static class SeoulAddressResult { + private String code; + private String name; + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncAssociateInfoResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncAssociateInfoResponseDto.java new file mode 100644 index 0000000..ba587e5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncAssociateInfoResponseDto.java @@ -0,0 +1,36 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.sync.entity.SyncType; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public record SyncAssociateInfoResponseDto( + Long syncId, + String syncType, + String type, + String image, + int userCnt, + int totalCnt, + String syncName, + String location, + String date, + String associate +) { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("M월 d일 (E) a h:mm"); + + public static SyncAssociateInfoResponseDto of(Long syncId, + SyncType syncType, + Type type, + String image, + int userCnt, + int totalCnt, + String syncName, + String location, + LocalDateTime date, + String associate){ + String formattedDate = date.format(DATE_TIME_FORMATTER); + return new SyncAssociateInfoResponseDto(syncId, String.valueOf(syncType), String.valueOf(type), image, userCnt, totalCnt, syncName, location, formattedDate, associate); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncCreateResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncCreateResponseDto.java new file mode 100644 index 0000000..5832351 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncCreateResponseDto.java @@ -0,0 +1,45 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class SyncCreateResponseDto { + private Long syncId; + private String link; + private String syncType; + private String parentCategory; + private String childCategory; + private String name; + private String image; + private String comment; + private String location; + private String date; + private int meeting_cnt; + private int member_min; + private int member_max; + + public static SyncCreateResponseDto of(Long syncId, String link, String syncType, + String parentCategory, String childCategory, + String name, String image, String comment, + String location, String date, int meeting_cnt, + int member_min, int member_max) { + return SyncCreateResponseDto.builder() + .syncId(syncId) + .link(link) + .syncType(syncType) + .parentCategory(parentCategory) + .childCategory(childCategory) + .name(name) + .image(image) + .comment(comment) + .location(location) + .date(date) + .meeting_cnt(meeting_cnt) + .member_min(member_min) + .member_max(member_max) + .build(); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncDetailResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncDetailResponseDto.java new file mode 100644 index 0000000..371a2ec --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncDetailResponseDto.java @@ -0,0 +1,66 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.sync.entity.SyncType; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public record SyncDetailResponseDto( + String syncName, + String syncImage, + String syncType, + String type, + String syncIntro, + String regularDate, + String date, + String location, + int userCnt, + int totalCnt, + String userImage, + String userName, + String university, + String userIntro, + Boolean isFull +) { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("M월 d일 (EE) a h:mm"); + public static SyncDetailResponseDto oneTimeOf(String syncName, + String syncImage, + SyncType syncType, + Type type, + String syncIntro, + LocalDateTime date, + String location, + int userCnt, + int totalCnt, + String userImage, + String userName, + String university, + String userIntro, + Boolean isFull){ + String formattedDate = date.format(DATE_TIME_FORMATTER); + return new SyncDetailResponseDto(syncName, syncImage, String.valueOf(syncType), String.valueOf(type), syncIntro, null, formattedDate, location, userCnt, totalCnt, userImage, userName, university, userIntro, isFull); + } + public static SyncDetailResponseDto longTimeOf(String syncName, + String syncImage, + SyncType syncType, + Type type, + String syncIntro, + String regularDay, + LocalTime regularTime, + LocalDateTime date, + String location, + int userCnt, + int totalCnt, + String userImage, + String userName, + String university, + String userIntro, + Boolean isFull){ + String formattedDate = date.format(DATE_TIME_FORMATTER); + String formattedRegularTime = regularTime.format(DateTimeFormatter.ofPattern("a h:mm")); + String regularDate = "매주 " + regularDay + " " + formattedRegularTime; + return new SyncDetailResponseDto(syncName, syncImage, String.valueOf(syncType), String.valueOf(type), syncIntro, regularDate, formattedDate, location, userCnt, totalCnt, userImage, userName, university, userIntro, isFull); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncGraphResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncGraphResponseDto.java new file mode 100644 index 0000000..ffe5829 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncGraphResponseDto.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import java.util.List; + +public record SyncGraphResponseDto( + List data, + String status + +) { + public static SyncGraphResponseDto of(List data, String status){ + return new SyncGraphResponseDto(data, status); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncInfoResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncInfoResponseDto.java new file mode 100644 index 0000000..664a34d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncInfoResponseDto.java @@ -0,0 +1,34 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.sync.entity.SyncType; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public record SyncInfoResponseDto( + Long syncId, + String syncType, + String type, + String image, + int userCnt, + int totalCnt, + String syncName, + String location, + String date +) { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("M월 d일 (EE) a h:mm"); + + public static SyncInfoResponseDto of(Long syncId, + SyncType syncType, + Type type, + String image, + int userCnt, + int totalCnt, + String syncName, + String location, + LocalDateTime date){ + String formattedDate = date.format(DATE_TIME_FORMATTER); + return new SyncInfoResponseDto(syncId, String.valueOf(syncType), String.valueOf(type), image, userCnt, totalCnt, syncName, location, formattedDate); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncReviewResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncReviewResponseDto.java new file mode 100644 index 0000000..0fb687c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncReviewResponseDto.java @@ -0,0 +1,25 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import java.time.Duration; +import java.time.LocalDateTime; + +public record SyncReviewResponseDto( + String image, + String name, + String university, + String content, + String date +) { + public static SyncReviewResponseDto of(String image, String name, String university, String content, LocalDateTime date){ + return new SyncReviewResponseDto(image, name, university, content, calculateTimeDifference(date)); + } + public static String calculateTimeDifference(LocalDateTime date) { + LocalDateTime now = LocalDateTime.now(); + Duration duration = Duration.between(date, now); + long months = duration.toDays() / 30; + if (months > 0) { + return months + "달 전"; + } + return "방금 전"; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncSaveResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncSaveResponseDto.java new file mode 100644 index 0000000..cc5e193 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/sync/service/dto/response/SyncSaveResponseDto.java @@ -0,0 +1,61 @@ +package com.kusitms29.backendH.api.sync.service.dto.response; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Builder +@Getter +public class SyncSaveResponseDto { + private long syncId; + private String userIntro; + private String syncIntro; + private String syncType; + private String syncName; + private String image; + private String location; + private LocalDateTime date; //일회성, 내친소 + + private String regularDay; //지속성 요일 + private LocalTime regularTime; //지속성 시간 + private LocalDateTime routineDate; //지속성 첫모임 + + private int member_min; + + private int member_max; + + private String type; //외국어, 엔터테인먼트, ... + + private String detailType; + + + public static SyncSaveResponseDto of(Long syncId, String userIntro, String syncIntro, + SyncType syncType, String syncName, + String image, String location, + LocalDateTime date, + String regularDay, LocalTime regularTime, LocalDateTime routineDate, + int member_min, int member_max, + Type type, String detailType) { + return SyncSaveResponseDto.builder() + .syncId(syncId) + .userIntro(userIntro) + .syncIntro(syncIntro) + .syncType(syncType.getStringSyncType()) + .syncName(syncName) + .image(image) + .location(location) + .date(date) + .regularDay(regularDay) + .regularTime(regularTime) + .routineDate(routineDate) + .member_min(member_min) + .member_max(member_max) + .type(type.getStringType()) + .detailType(detailType) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/controller/AuthController.java b/Main/src/main/java/com/kusitms29/backendH/api/user/controller/AuthController.java new file mode 100644 index 0000000..eebf638 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/controller/AuthController.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.api.user.controller; + +import com.kusitms29.backendH.api.user.service.AuthService; +import com.kusitms29.backendH.api.user.service.dto.request.UserSignInRequestDto; +import com.kusitms29.backendH.api.user.service.dto.response.UserAuthResponseDto; +import com.kusitms29.backendH.global.common.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@RestController +public class AuthController { + private final AuthService authService; + @PostMapping("/signin") + public ResponseEntity> signIn(@RequestHeader("Authorization") final String authToken, + @RequestHeader String fcmToken, + @RequestBody final UserSignInRequestDto requestDto) { + final UserAuthResponseDto responseDto = authService.signIn(requestDto, authToken, fcmToken); + return SuccessResponse.ok(responseDto); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/controller/MyPageController.java b/Main/src/main/java/com/kusitms29/backendH/api/user/controller/MyPageController.java new file mode 100644 index 0000000..4e62b3d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/controller/MyPageController.java @@ -0,0 +1,55 @@ +package com.kusitms29.backendH.api.user.controller; + +import com.kusitms29.backendH.api.sync.service.dto.response.SyncInfoResponseDto; +import com.kusitms29.backendH.api.user.service.MyPageService; +import com.kusitms29.backendH.api.user.service.dto.request.CreateReviewRequest; +import com.kusitms29.backendH.api.user.service.dto.request.EditProfileRequest; +import com.kusitms29.backendH.api.user.service.dto.response.CreateReviewResponse; +import com.kusitms29.backendH.api.user.service.dto.response.UserInfoResponseDto; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.config.auth.UserId; +import com.kusitms29.backendH.infra.utils.TranslateUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/mypage") +public class MyPageController { + private final MyPageService myPageService; + private final TranslateUtil translateUtil; + @GetMapping("/mysync") + public ResponseEntity> getMySyncList(@UserId Long userId,@RequestParam(name = "take",defaultValue = "0") int take) { + List< SyncInfoResponseDto> syncInfoResponseDtos = myPageService.getMySyncList(userId,take); + return SuccessResponse.ok(syncInfoResponseDtos); + } + @GetMapping("/join") + public ResponseEntity> getJoinSyncList(@UserId Long userId,@RequestParam(name = "take",defaultValue = "0") int take) { + List< SyncInfoResponseDto> syncInfoResponseDtos = myPageService.getJoinSyncList(userId,take); + return SuccessResponse.ok(syncInfoResponseDtos); + } + @GetMapping + public ResponseEntity> getMyInfo(@UserId Long userId, @RequestParam (name = "language", defaultValue = "한국어")String language) { + UserInfoResponseDto userInfoResponseDto = myPageService.getMyInfo(userId); + if (language.equals("영어"))userInfoResponseDto=translateUtil.translateObject(userInfoResponseDto); + return SuccessResponse.ok(userInfoResponseDto); + } + @PostMapping("/review") + public ResponseEntity> createReview(@UserId Long userId, @RequestBody CreateReviewRequest createReviewRequest) { + CreateReviewResponse createReviewResponse = myPageService.createReview(userId,createReviewRequest); + return SuccessResponse.created(createReviewResponse); + } + @GetMapping("/bookmark") + public ResponseEntity> getBookMarkSyncList(@UserId Long userId, @RequestParam(name = "take",defaultValue = "0") int take) { + List userInfoResponseDto = myPageService.getBookMarkSyncList(userId, take); + return SuccessResponse.ok(userInfoResponseDto); + } + @PatchMapping + public ResponseEntity> editBoard(@UserId Long userId, @ModelAttribute EditProfileRequest editProfileRequest) { + myPageService.editProfile(userId, editProfileRequest); + return SuccessResponse.ok("UPDATE"); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/controller/OnBoardingController.java b/Main/src/main/java/com/kusitms29/backendH/api/user/controller/OnBoardingController.java new file mode 100644 index 0000000..c6905bf --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/controller/OnBoardingController.java @@ -0,0 +1,69 @@ +package com.kusitms29.backendH.api.user.controller; + +import com.kusitms29.backendH.api.user.service.OnBoardingService; +import com.kusitms29.backendH.api.user.service.dto.request.CountryCalloutRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.OnBoardingRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.UniversityRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.schoolEmail.SchoolEmailRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.schoolEmail.SchoolEmailVerificationRequestDto; +import com.kusitms29.backendH.api.user.service.dto.response.OnBoardingResponseDto; +import com.kusitms29.backendH.api.user.service.dto.response.schoolEmail.CalloutErrorResponse; +import com.kusitms29.backendH.api.user.service.dto.response.schoolEmail.CalloutSchoolEmailVerificationResponseDto; +import com.kusitms29.backendH.global.common.SuccessResponse; +import com.kusitms29.backendH.infra.config.auth.UserId; +import com.kusitms29.backendH.infra.external.CountryDataClient; +import com.kusitms29.backendH.infra.external.SchoolEmailClient; +import com.kusitms29.backendH.infra.external.UniversityClient; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +@RequestMapping("/api/user") +@RequiredArgsConstructor +@RestController +public class OnBoardingController { + private final OnBoardingService onBoardingService; + private final UniversityClient universityClient; + private final CountryDataClient countryDataClient; + private final SchoolEmailClient schoolEmailClient; + + @PostMapping("/onboarding") + public ResponseEntity> onboarding(@UserId Long userId, + @RequestPart("profileImage") MultipartFile profileImage, + @RequestPart("onBoardingRequest") OnBoardingRequestDto requestDto) { + onBoardingService.onBoardingUser(userId, profileImage, requestDto); + return SuccessResponse.created("success"); + } + @PostMapping("/valid-university") + public ResponseEntity> isItValidUniversity(@RequestBody UniversityRequestDto requestDto) { + universityClient.isValidUniversity(requestDto.getUnivName()); + return SuccessResponse.ok(true); //주석 + } + + @PostMapping("/countries") + public ResponseEntity> getCountries(@RequestBody CountryCalloutRequestDto requestDto) { + List countryNames = countryDataClient.listOfCountries(requestDto.getPage(), requestDto.getPerPage(), requestDto.getLanguage()); + return SuccessResponse.ok(countryNames); + } + + + @PostMapping("/school-emails/verification-requests") + public ResponseEntity> sendMessageToSchool(@RequestBody SchoolEmailRequestDto requestDto) { + CalloutErrorResponse responseDto = schoolEmailClient.callOutSendSchoolEmail(requestDto); + return SuccessResponse.ok(responseDto.isSuccess()); + } + + @PostMapping("/school-emails/verifications") + public ResponseEntity> verificationSchoolEmail(@RequestBody SchoolEmailVerificationRequestDto requestDto) { + CalloutSchoolEmailVerificationResponseDto responseDto = schoolEmailClient.callOutAuthSchoolEmail(requestDto); + return SuccessResponse.ok(responseDto); + } + + @PostMapping("/school-emails/reset") + public ResponseEntity> resetForTryEmailTest() { + CalloutErrorResponse responseDto = schoolEmailClient.clearAuthCode(); + return SuccessResponse.ok(responseDto.isSuccess()); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/AuthService.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/AuthService.java new file mode 100644 index 0000000..285f189 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/AuthService.java @@ -0,0 +1,104 @@ +package com.kusitms29.backendH.api.user.service; + +import com.kusitms29.backendH.api.user.service.dto.request.UserSignInRequestDto; +import com.kusitms29.backendH.api.user.service.dto.response.UserAuthResponseDto; +import com.kusitms29.backendH.infra.external.fcm.service.PushNotificationService; +import com.kusitms29.backendH.domain.user.auth.PlatformUserInfo; +import com.kusitms29.backendH.domain.user.auth.RestTemplateProvider; +import com.kusitms29.backendH.domain.user.entity.Platform; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.repository.RefreshTokenRepository; +import com.kusitms29.backendH.domain.user.service.UserModifier; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import com.kusitms29.backendH.infra.config.auth.JwtProvider; +import com.kusitms29.backendH.infra.config.auth.TokenInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import static com.kusitms29.backendH.domain.user.entity.Platform.getEnumPlatformFromStringPlatform; +import static com.kusitms29.backendH.domain.user.entity.RefreshToken.createRefreshToken; +import static com.kusitms29.backendH.global.error.ErrorCode.FCMTOKEN_NOT_FOUND; + + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class AuthService { + private final JwtProvider jwtProvider; + private final RestTemplateProvider restTemplateProvider; + private final RefreshTokenRepository refreshTokenRepository; + private final PushNotificationService pushNotificationService; + private final UserReader userReader; + private final UserModifier userModifier; + + public UserAuthResponseDto signIn(UserSignInRequestDto userSignInRequestDto, String authToken, String fcmToken) { + if (fcmToken == null || fcmToken.isEmpty()) { + throw new EntityNotFoundException(FCMTOKEN_NOT_FOUND); + } + Platform platform = getEnumPlatformFromStringPlatform(userSignInRequestDto.getPlatform()); + PlatformUserInfo platformUser = getPlatformUserInfoFromRestTemplate(platform, authToken); + User getUser = saveUser(platformUser, platform); + saveFcmToken(getUser, fcmToken); + Boolean isFirstLogin = Objects.isNull(getUser.getPlatform()) ? Boolean.TRUE : Boolean.FALSE; + TokenInfo tokenInfo = issueAccessTokenAndRefreshToken(getUser); + updateRefreshToken(tokenInfo.getRefreshToken(), getUser); + return UserAuthResponseDto.of(getUser, tokenInfo, isFirstLogin); + } + + public void signOut(Long userId) { + User findUser = getUserFromUserId(userId); + deleteRefreshToken(findUser); + } + + private void deleteRefreshToken(User user) { + user.updateRefreshToken(null); + refreshTokenRepository.deleteById(user.getId()); + } + + private User saveUser(PlatformUserInfo platformUserInfo, Platform platform) { + User createdUser = getUserByPlatformUserInfo(platformUserInfo, platform); + return userModifier.save(createdUser); + } + + private void updateRefreshToken(String refreshToken, User user) { + user.updateRefreshToken(refreshToken); + refreshTokenRepository.save(createRefreshToken(user.getId(), refreshToken)); + } + + private TokenInfo issueAccessTokenAndRefreshToken(User user) { + return jwtProvider.issueToken(user.getId()); + } + + private User getUserFromUserId(Long userId) { + return userReader.findByUserId(userId); + } + + private User getUserByPlatformUserInfo(PlatformUserInfo platformUserInfo, Platform platform) { + Optional optionalUser = userReader.findByPlatformId(platformUserInfo.getId()); + return optionalUser.orElseGet(() -> User.createUser(platformUserInfo, platform, generateRandomUuid(platformUserInfo))); + } + private void saveFcmToken(User getUser, String fcmToken) { + pushNotificationService.saveToken(String.valueOf(getUser.getId()), fcmToken); + } + + private PlatformUserInfo getPlatformUserInfoFromRestTemplate(Platform platform, String authToken) { + return restTemplateProvider.getUserInfoUsingRestTemplate(platform, authToken); + } + + private String generateRandomUuid(PlatformUserInfo platformUserInfo) { + UUID randomUuid = UUID.randomUUID(); + String uuidAsString = randomUuid.toString().replace("-", ""); + return platformUserInfo.getId() + "_" + uuidAsString.substring(0, 6); + } + +} + + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/MyPageService.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/MyPageService.java new file mode 100644 index 0000000..a860f5f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/MyPageService.java @@ -0,0 +1,113 @@ +package com.kusitms29.backendH.api.user.service; + +import com.kusitms29.backendH.api.sync.service.dto.response.SyncInfoResponseDto; +import com.kusitms29.backendH.api.user.service.dto.request.CreateReviewRequest; +import com.kusitms29.backendH.api.user.service.dto.request.EditProfileRequest; +import com.kusitms29.backendH.api.user.service.dto.response.CreateReviewResponse; +import com.kusitms29.backendH.api.user.service.dto.response.UserInfoResponseDto; +import com.kusitms29.backendH.domain.category.entity.Category; +import com.kusitms29.backendH.domain.category.service.CategoryReader; +import com.kusitms29.backendH.domain.category.service.UserCategoryModifier; +import com.kusitms29.backendH.domain.category.service.UserCategoryReader; +import com.kusitms29.backendH.domain.sync.entity.*; +import com.kusitms29.backendH.domain.sync.service.*; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserModifier; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.infra.config.AwsS3Service; +import com.kusitms29.backendH.infra.utils.ListUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.kusitms29.backendH.domain.category.entity.UserCategory.createUserCategory; +import static com.kusitms29.backendH.domain.sync.entity.Gender.getEnumFROMStringGender; +import static com.kusitms29.backendH.domain.sync.entity.SyncType.getEnumFROMStringSyncType; + +@Service +@RequiredArgsConstructor +public class MyPageService { + private final ParticipationManager participationManager; + private final ListUtils listUtils; + private final SyncReader syncReader; + private final ParticipationReader participationReader; + private final UserReader userReader; + private final SyncReviewAppender syncReviewAppender; + private final FavoriteSyncReader favoriteSyncReader; + private final UserCategoryModifier userCategoryModifier; + private final AwsS3Service awsS3Service; + private final CategoryReader categoryReader; + private final UserCategoryReader userCategoryReader; + public List getMySyncList(Long userId, int take){ + List syncList = syncReader.findAllByUserId(userId); + List syncInfoResponseDtos = syncList.stream() + .map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(); + return listUtils.getListByTake(syncInfoResponseDtos, take); + } + public List getJoinSyncList(Long userId, int take){ + List participations = participationReader.findAllByUserId(userId); + List syncList = participations.stream().map(participation -> syncReader.findById(participation.getSync().getId())).toList(); + List syncInfoResponseDtos = syncList.stream() + .map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(); + return listUtils.getListByTake(syncInfoResponseDtos, take); + } + public UserInfoResponseDto getMyInfo(Long userId){ + List detailTypes = userCategoryReader.findAllByUserId(userId).stream().map(userCategory -> userCategory.getCategory().getName()).toList(); + return UserInfoResponseDto.of(userReader.findByUserId(userId),detailTypes); + } + public CreateReviewResponse createReview(Long userId, CreateReviewRequest createReviewRequest){ + SyncReview syncReview = SyncReview.createReview(User.from(userId),Sync.from(createReviewRequest.syncId()), createReviewRequest.content() ); + syncReview = syncReviewAppender.createReview(syncReview); + return CreateReviewResponse.of(syncReview); + } + public List getBookMarkSyncList(Long userId, int take){ + List favoriteSyncs = favoriteSyncReader.findAllByUserId(userId); + List syncList = favoriteSyncs.stream().map(favoriteSync -> syncReader.findById(favoriteSync.getSync().getId())).toList(); + List syncInfoResponseDtos = syncList.stream() + .map( sync -> SyncInfoResponseDto.of( + sync.getId(), + sync.getSyncType(), + sync.getType(), + sync.getImage(), + participationManager.countParticipationBySyncId(sync.getId()), + sync.getMember_max(), + sync.getSyncName(), + sync.getLocation(), + sync.getDate() + )).toList(); + return listUtils.getListByTake(syncInfoResponseDtos, take); + } + @Transactional + public void editProfile(Long userId, EditProfileRequest editProfileRequest){ + User user = userReader.findByUserId(userId); + String image = awsS3Service.uploadImage(editProfileRequest.image()); + user.updateProfile(image,editProfileRequest.name(), getEnumFROMStringGender(editProfileRequest.gender()), getEnumFROMStringSyncType(editProfileRequest.syncType())); + userCategoryModifier.deleteAllByUserId(user.getId()); + List categories = editProfileRequest.detailTypes().stream().map( + detailType -> categoryReader.findByName(detailType)) + .toList(); + userCategoryModifier.saveAll(categories.stream().map(category -> createUserCategory(user,category)).toList()); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/OnBoardingService.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/OnBoardingService.java new file mode 100644 index 0000000..a3e891a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/OnBoardingService.java @@ -0,0 +1,76 @@ +package com.kusitms29.backendH.api.user.service; + +import com.kusitms29.backendH.api.user.service.dto.request.OnBoardingRequestDto; +import com.kusitms29.backendH.api.user.service.dto.response.OnBoardingResponseDto; +import com.kusitms29.backendH.domain.category.entity.Category; +import com.kusitms29.backendH.domain.category.service.CategoryReader; +import com.kusitms29.backendH.domain.category.service.UserCategoryModifier; +import com.kusitms29.backendH.domain.sync.entity.Gender; +import com.kusitms29.backendH.domain.sync.entity.Language; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.category.entity.UserCategory; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.infra.config.AwsS3Service; +import com.kusitms29.backendH.infra.external.UniversityClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.kusitms29.backendH.domain.category.entity.UserCategory.createUserCategory; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class OnBoardingService { + private final UniversityClient universityClient; + private final AwsS3Service awsS3Service; + + private final UserReader userReader; + private final CategoryReader categoryReader; + private final UserCategoryModifier userCategoryModifier; + + @Transactional + public void onBoardingUser(Long userId, MultipartFile profileImage, OnBoardingRequestDto requestDto) { + User user = userReader.findByUserId(userId); + + Language lan = Language.getEnumLanguageFromStringLanguage(requestDto.getLanguage()); + String imageUrl = awsS3Service.uploadImage(profileImage); + Gender gen = Gender.getEnumFROMStringGender(requestDto.getGender()); + SyncType syncType = SyncType.getEnumFROMStringSyncType(requestDto.getSyncType()); + universityClient.isValidUniversity(requestDto.getUniversity()); + + user.updateOnBoardingWithoutCategory(lan.name(), imageUrl, requestDto.getUserName(), + requestDto.getCountryName(), gen.name(), requestDto.getUniversity(), requestDto.getEmail(), syncType.name()); + + List categories = requestDto.getDetailTypes().stream().map( + detailType -> categoryReader.findByName(detailType)) + .toList(); + userCategoryModifier.saveAll(categories.stream().map(category -> createUserCategory(user,category)).toList()); + } + +// private List createUserCategory(User user, Map categoryMap) { +// List categoryNames = new ArrayList<>(); +// for (Map.Entry entry : categoryMap.entrySet()) { +// if (entry.getValue()) { +// Category category = categoryReader.findByName(entry.getKey()); +// UserCategory userCategory = UserCategory.builder() +// .user(user) +// .category(category) +// .build(); +// userCategoryModifier.save(userCategory); +// categoryNames.add(category.getName()); +// } +// } +// return categoryNames; +// } + +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/UserService.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/UserService.java new file mode 100644 index 0000000..9827bec --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/UserService.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.api.user.service; + +import com.kusitms29.backendH.api.community.service.dto.response.BannerImageResponseDto; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class UserService { + private final UserReader userReader; + + public BannerImageResponseDto getLoginUserImage(Long userId) { + User user = userReader.findByUserId(userId); + String image = user.getProfile(); + return BannerImageResponseDto.of(image); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CategoryRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CategoryRequestDto.java new file mode 100644 index 0000000..b5b2341 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CategoryRequestDto.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CategoryRequestDto { + private Map foreignLanguage; + private Map cultureArt; + private Map travelCompanion; + private Map activity; + private Map foodAndDrink; + private Map etc; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CountryCalloutRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CountryCalloutRequestDto.java new file mode 100644 index 0000000..cb5ecbb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CountryCalloutRequestDto.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Setter +public class CountryCalloutRequestDto { + private Integer page; + private Integer perPage; + private String language; +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CreateReviewRequest.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CreateReviewRequest.java new file mode 100644 index 0000000..827b011 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/CreateReviewRequest.java @@ -0,0 +1,7 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +public record CreateReviewRequest( + Long syncId, + String content +) { +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EditProfileRequest.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EditProfileRequest.java new file mode 100644 index 0000000..b069deb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EditProfileRequest.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +public record EditProfileRequest( + MultipartFile image, + String name, + String gender, + String syncType, + List detailTypes +) { +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EmailVerificationRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EmailVerificationRequestDto.java new file mode 100644 index 0000000..842c15d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/EmailVerificationRequestDto.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class EmailVerificationRequestDto { + private String email; + private String code; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/OnBoardingRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/OnBoardingRequestDto.java new file mode 100644 index 0000000..4770a32 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/OnBoardingRequestDto.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class OnBoardingRequestDto { + private String language; + private String userName; + private String countryName; + private String gender; + private String university; + private String email; + private String syncType; + private List detailTypes; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/ReceiverInfoRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/ReceiverInfoRequestDto.java new file mode 100644 index 0000000..99fe648 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/ReceiverInfoRequestDto.java @@ -0,0 +1,11 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ReceiverInfoRequestDto { + private String toEmail; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UniversityRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UniversityRequestDto.java new file mode 100644 index 0000000..53d3f8c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UniversityRequestDto.java @@ -0,0 +1,11 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Setter +public class UniversityRequestDto { + String univName; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UserSignInRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UserSignInRequestDto.java new file mode 100644 index 0000000..c4e02b6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/UserSignInRequestDto.java @@ -0,0 +1,11 @@ +package com.kusitms29.backendH.api.user.service.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class UserSignInRequestDto { + private String platform; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailRequestDto.java new file mode 100644 index 0000000..2e66e7d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailRequestDto.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.api.user.service.dto.request.schoolEmail; + +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Setter +public class CalloutSchoolEmailRequestDto { + private String key; + private String email; + private String univName; + private Boolean univ_check; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailVerificationRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailVerificationRequestDto.java new file mode 100644 index 0000000..83a771d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/CalloutSchoolEmailVerificationRequestDto.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.api.user.service.dto.request.schoolEmail; + +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Setter +public class CalloutSchoolEmailVerificationRequestDto { + private String key; + private String email; + private String univName; + private int code; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailRequestDto.java new file mode 100644 index 0000000..e296d4f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailRequestDto.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.api.user.service.dto.request.schoolEmail; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class SchoolEmailRequestDto { + private String email; + private String univName; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailVerificationRequestDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailVerificationRequestDto.java new file mode 100644 index 0000000..8524b0a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/request/schoolEmail/SchoolEmailVerificationRequestDto.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.api.user.service.dto.request.schoolEmail; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class SchoolEmailVerificationRequestDto { + private String univName; + private String email; + private int code; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryDataDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryDataDto.java new file mode 100644 index 0000000..f65728a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryDataDto.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class CountryDataDto { + private String ISO_alpha2; + private String ISO_alpha3; + private Integer ISO_numeric; + private String 대륙명_공통_대륙코드; + private String 대륙명_행정표준코드; + private String 대륙명_외교부_직제; + private String 영문명; + private String 한글명; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryResponseDto.java new file mode 100644 index 0000000..b669a5d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CountryResponseDto.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class CountryResponseDto { + private Integer currentCount; + private List data; + private Integer matchCount; + private Integer page; + private Integer perPage; + private Integer totalCount; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CreateReviewResponse.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CreateReviewResponse.java new file mode 100644 index 0000000..c1671f0 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/CreateReviewResponse.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import com.kusitms29.backendH.domain.sync.entity.SyncReview; + +public record CreateReviewResponse( + Long syncId, + Long userId +) { + public static CreateReviewResponse of(SyncReview syncReview){ + return new CreateReviewResponse(syncReview.getSync().getId(), syncReview.getUser().getId()); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/EmailVerificationResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/EmailVerificationResponseDto.java new file mode 100644 index 0000000..c2a44ea --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/EmailVerificationResponseDto.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class EmailVerificationResponseDto { + + private boolean authResult; + + public static EmailVerificationResponseDto of(Boolean authResult) { + return EmailVerificationResponseDto.builder() + .authResult(authResult) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/OnBoardingResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/OnBoardingResponseDto.java new file mode 100644 index 0000000..ca319e4 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/OnBoardingResponseDto.java @@ -0,0 +1,35 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class OnBoardingResponseDto { + private String language; + private String profileImage; + private String userName; + private String countryName; + private String gender; + private String university; + private String email; + private String syncType; + private List detailTypes; + + public static OnBoardingResponseDto of(String language, String profileImage, String userName, String countryName, String gender, + String university, String email, String syncType, List detailTypes) { + return OnBoardingResponseDto.builder() + .language(language) + .profileImage(profileImage) + .userName(userName) + .countryName(countryName) + .gender(gender) + .university(university) + .email(email) + .syncType(syncType) + .detailTypes(detailTypes) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserAuthResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserAuthResponseDto.java new file mode 100644 index 0000000..a51bd4a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserAuthResponseDto.java @@ -0,0 +1,32 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.infra.config.auth.TokenInfo; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class UserAuthResponseDto { + private Long userId; + private String email; + private String name; + private String picture; + private String accessToken; + private String refreshToken; + private Boolean isFirst; + private String sessionId; + + public static UserAuthResponseDto of(User user, TokenInfo token, Boolean isFirst) { + return UserAuthResponseDto.builder() + .userId(user.getId()) + .email(user.getEmail()) + .name(user.getUserName()) + .picture(user.getProfile()) + .isFirst(isFirst) + .accessToken(token.getAccessToken()) + .refreshToken(token.getRefreshToken()) + .sessionId(user.getSessionId()) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserInfoResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserInfoResponseDto.java new file mode 100644 index 0000000..24290bc --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserInfoResponseDto.java @@ -0,0 +1,39 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import com.kusitms29.backendH.domain.user.entity.User; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class UserInfoResponseDto { + private Long userId; + private String image; + private String name; + private String university; + private String syncType; + private List detailTypes; + private String gender; + + public UserInfoResponseDto() { + // 기본 생성자 + } + + public UserInfoResponseDto(Long userId, String image, String name, String university, String syncType, List detailTypes, String gender) { + this.userId = userId; + this.image = image; + this.name = name; + this.university = university; + this.syncType = syncType; + this.detailTypes = detailTypes; + this.gender = gender; + } + + public static UserInfoResponseDto of(User user, List detailTypes) { + return new UserInfoResponseDto(user.getId(), user.getProfile(), user.getUserName(), user.getUniversity(), String.valueOf(user.getSyncType()), detailTypes, String.valueOf(user.getGender())); + } + +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserSignUpResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserSignUpResponseDto.java new file mode 100644 index 0000000..29177e2 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/UserSignUpResponseDto.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.api.user.service.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class UserSignUpResponseDto { + private String name; + + public static UserSignUpResponseDto of(String name) { + return UserSignUpResponseDto.builder() + .name(name) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutErrorResponse.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutErrorResponse.java new file mode 100644 index 0000000..7e03d6c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutErrorResponse.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.api.user.service.dto.response.schoolEmail; + +import lombok.*; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CalloutErrorResponse { + private String status; + private boolean success; + private String message; +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutSchoolEmailVerificationResponseDto.java b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutSchoolEmailVerificationResponseDto.java new file mode 100644 index 0000000..5da2007 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/api/user/service/dto/response/schoolEmail/CalloutSchoolEmailVerificationResponseDto.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.api.user.service.dto.response.schoolEmail; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class CalloutSchoolEmailVerificationResponseDto { + private boolean success; + private String univName; + private String certified_email; + private String certified_date; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Category.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Category.java new file mode 100644 index 0000000..74788c5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Category.java @@ -0,0 +1,22 @@ +package com.kusitms29.backendH.domain.category.entity; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "category") +@Entity +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "category_id") + private Long id; + + private String name; + @Enumerated(EnumType.STRING) + private Type type; + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Type.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Type.java new file mode 100644 index 0000000..366be8b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/Type.java @@ -0,0 +1,37 @@ +package com.kusitms29.backendH.domain.category.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_TYPE; + +@RequiredArgsConstructor +@Getter +public enum Type { + //외국어 : 언어 교환, 튜터링, 스터디, 기타 + //문화/예술 : 문화/예술, 영화, 드라마, 미술/디자인, 공연/전시, 음악, 기타 + //여행/동행 : 관광지, 자연, 휴양, 기타 + //액티비티 : 러닝/산책, 등산, 클라이밍, 자전거, 축구, 서핑, 테니스, 볼링, 탁구, 기타 + //푸드드링크 : 맛집, 카페, 술, 기타 + //기타 + LANGUAGE("외국어"), + ENTERTAINMENT("문화/예술"), + TRAVEL("여행/동행"), + ACTIVITY("액티비티"), + FOOD("푸드드링크"), + ETC("기타"); + + private final String stringType; + public static Type getEnumTypeFromStringType(String stringType) { + if (stringType == null) { + return null; + } + return Arrays.stream(values()) + .filter(type -> type.stringType.equals(stringType)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_TYPE)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/UserCategory.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/UserCategory.java new file mode 100644 index 0000000..d4dd2e5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/entity/UserCategory.java @@ -0,0 +1,34 @@ +package com.kusitms29.backendH.domain.category.entity; + +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "user_category") +@Entity +public class UserCategory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_category_id") + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; + + public void updateUserCategory(User user, Category category){ + this.user = user; + this.category = category; + } + public static UserCategory createUserCategory(User user, Category category){ + return UserCategory.builder() + .user(user) + .category(category) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/repository/CategoryRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/repository/CategoryRepository.java new file mode 100644 index 0000000..4de176d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/repository/CategoryRepository.java @@ -0,0 +1,9 @@ +package com.kusitms29.backendH.domain.category.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import com.kusitms29.backendH.domain.category.entity.Category; + +import java.util.Optional; + +public interface CategoryRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/repository/UserCategoryRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/repository/UserCategoryRepository.java new file mode 100644 index 0000000..4a628a2 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/repository/UserCategoryRepository.java @@ -0,0 +1,11 @@ +package com.kusitms29.backendH.domain.category.repository; + +import com.kusitms29.backendH.domain.category.entity.UserCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface UserCategoryRepository extends JpaRepository { + List findAllByUserId(Long userId); + void deleteAllByUserId(Long userId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/service/CategoryReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/CategoryReader.java new file mode 100644 index 0000000..7c7739e --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/CategoryReader.java @@ -0,0 +1,24 @@ +package com.kusitms29.backendH.domain.category.service; + +import com.kusitms29.backendH.domain.category.entity.Category; +import com.kusitms29.backendH.domain.category.repository.CategoryRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.kusitms29.backendH.global.error.ErrorCode.CATEGORY_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CategoryReader { + private final CategoryRepository categoryRepository; + + public Category findByName(String name) { + return categoryRepository.findByName(name) + .orElseThrow(() -> new EntityNotFoundException(CATEGORY_NOT_FOUND)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryManager.java new file mode 100644 index 0000000..30ff267 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryManager.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.domain.category.service; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.category.entity.UserCategory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserCategoryManager { + public List getTypeByUserCategories(List userCategories){ + return userCategories.stream().map(userCategory -> userCategory.getCategory().getType()).toList(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryModifier.java new file mode 100644 index 0000000..b411128 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryModifier.java @@ -0,0 +1,26 @@ +package com.kusitms29.backendH.domain.category.service; + +import com.kusitms29.backendH.domain.category.entity.UserCategory; +import com.kusitms29.backendH.domain.category.repository.CategoryRepository; +import com.kusitms29.backendH.domain.category.repository.UserCategoryRepository; +import com.kusitms29.backendH.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class UserCategoryModifier { + private final UserCategoryRepository userCategoryRepository; + + public void save(UserCategory userCategory) { + userCategoryRepository.save(userCategory); + } + public void deleteAllByUserId(Long userId){ userCategoryRepository.deleteAllByUserId(userId);} + public void saveAll(List userCategories){ userCategoryRepository.saveAll(userCategories);} +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryReader.java new file mode 100644 index 0000000..6751355 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/category/service/UserCategoryReader.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.domain.category.service; + +import com.kusitms29.backendH.domain.category.entity.UserCategory; +import com.kusitms29.backendH.domain.category.repository.UserCategoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserCategoryReader { + private final UserCategoryRepository userCategoryRepository; + public List findAllByUserId(Long userId){ + return userCategoryRepository.findAllByUserId(userId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatContent.java b/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatContent.java new file mode 100644 index 0000000..080ce0a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatContent.java @@ -0,0 +1,24 @@ +package com.kusitms29.backendH.domain.chat.entity; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class ChatContent { + private String userName; + private String content; + private LocalDateTime time; + + public static ChatContent createChatContent(String userName, String content, Room room) { + ChatContent chatContent = ChatContent.builder() + .userName(userName) + .content(content) + .time(LocalDateTime.now()) + .build(); + room.addChatContent(chatContent); + return chatContent; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatUser.java b/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatUser.java new file mode 100644 index 0000000..16e222c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/ChatUser.java @@ -0,0 +1,21 @@ +package com.kusitms29.backendH.domain.chat.entity; + +import com.kusitms29.backendH.domain.user.entity.User; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ChatUser { + private String sessionId; + private String name; + private String profile; + + public static ChatUser createChatUser(User user) { + return ChatUser.builder() + .sessionId(user.getSessionId()) + .name(user.getUserName()) + .profile(user.getProfile()) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/Room.java b/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/Room.java new file mode 100644 index 0000000..c2241b6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/chat/entity/Room.java @@ -0,0 +1,56 @@ +package com.kusitms29.backendH.domain.chat.entity; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Builder +@Document(collection = "room") +public class Room { + @Id + private String roomId; + private String roomName; + private String roomSession; + private String syncName; + + @Builder.Default + private List chatUserList = new ArrayList<>(); + @Builder.Default + private List chatContentList = new ArrayList<>(); + + public static Room createRoom(List users,String roomName) { + Room room = Room.builder(). + roomName(roomName). + build(); + for(ChatUser chatUser : users){ + room.addChatRoom(chatUser); + } + return room; + } + public static Room createNewRoom(String roomName) { + Room room = Room.builder() + .roomName(roomName) + .build(); + return room; + } + public void addChatContent(ChatContent content) { + this.chatContentList.add(content); + } + + public void addChatRoom(ChatUser chatUser) { + this.chatUserList.add(chatUser); + } + public Room(String roomId, String roomName, String roomSession, String syncName, List chatUserList, List chatContentList) { + this.roomId = roomId; + this.roomName = roomName; + this.roomSession = roomSession; + this.syncName = syncName; + this.chatUserList = chatUserList != null ? chatUserList : new ArrayList<>(); + this.chatContentList = chatContentList != null ? chatContentList : new ArrayList<>(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/chat/repository/RoomRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/chat/repository/RoomRepository.java new file mode 100644 index 0000000..8f1731f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/chat/repository/RoomRepository.java @@ -0,0 +1,7 @@ +package com.kusitms29.backendH.domain.chat.repository; + +import com.kusitms29.backendH.domain.chat.entity.Room; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface RoomRepository extends MongoRepository { +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/chat/service/RoomAppender.java b/Main/src/main/java/com/kusitms29/backendH/domain/chat/service/RoomAppender.java new file mode 100644 index 0000000..87c28b4 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/chat/service/RoomAppender.java @@ -0,0 +1,40 @@ +package com.kusitms29.backendH.domain.chat.service; + +import com.kusitms29.backendH.domain.chat.entity.ChatUser; +import com.kusitms29.backendH.domain.chat.entity.Room; +import com.kusitms29.backendH.domain.chat.repository.RoomRepository; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.infra.external.fcm.service.PushNotificationService; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class RoomAppender { + private final RoomRepository roomRepository; + private final PushNotificationService pushNotificationService; + @Transactional + public void createRoom(List userList, Boolean isPossible, Long syncId){ + if (isPossible) { + Room room = roomRepository.save( + Room.createRoom(userList.stream().map( + user -> ChatUser.createChatUser(user) ) + .toList(), + generateRandomUuid(syncId) + ) + ); + + //채팅방 개설 알림 + pushNotificationService.sendChatRoomNotice(userList, syncId, room.getRoomSession()); + } + } + private String generateRandomUuid(Long syncId) { + UUID randomUuid = UUID.randomUUID(); + String uuidAsString = randomUuid.toString().replace("-", ""); + return syncId + "_" + uuidAsString.substring(0, 6); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Comment.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Comment.java new file mode 100644 index 0000000..02884c3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Comment.java @@ -0,0 +1,35 @@ +package com.kusitms29.backendH.domain.comment.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "comment") +@Entity +public class Comment extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + private String content; + + @ColumnDefault("0") + @Builder.Default() + private int reported = 0; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/CommentLike.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/CommentLike.java new file mode 100644 index 0000000..5a6c9b1 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/CommentLike.java @@ -0,0 +1,27 @@ +package com.kusitms29.backendH.domain.comment.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "commentLike") +@Entity +public class CommentLike extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_like_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Reply.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Reply.java new file mode 100644 index 0000000..5c16168 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/Reply.java @@ -0,0 +1,34 @@ +package com.kusitms29.backendH.domain.comment.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "reply") +@Entity +public class Reply extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "reply_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; + + private String content; + + @ColumnDefault("0") + @Builder.Default() + private int reported = 0; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/ReplyLike.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/ReplyLike.java new file mode 100644 index 0000000..5df53ba --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/entity/ReplyLike.java @@ -0,0 +1,26 @@ +package com.kusitms29.backendH.domain.comment.entity; + +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "reply_like") +@Entity +public class ReplyLike { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "reply_like_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reply_id") + private Reply reply; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentLikeRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentLikeRepository.java new file mode 100644 index 0000000..ed0ed7f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentLikeRepository.java @@ -0,0 +1,21 @@ +package com.kusitms29.backendH.domain.comment.repository; + +import com.kusitms29.backendH.domain.comment.entity.CommentLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CommentLikeRepository extends JpaRepository { + int countByCommentId(Long commentId); + + boolean existsByCommentIdAndUserId(Long commentId, Long userId); + + Optional findByCommentIdAndUserId(Long commentId, Long userId); + + List findByCommentId(Long commentId); + + void deleteAllByCommentId(Long commentId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..733dc29 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/CommentRepository.java @@ -0,0 +1,19 @@ +package com.kusitms29.backendH.domain.comment.repository; + +import com.kusitms29.backendH.domain.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + int countByPostId(Long postId); + List findByPostId(Long postId); + + @Modifying + @Transactional + @Query("UPDATE Comment c SET c.reported = c.reported + 1 WHERE c.id = :commentId") + void increaseReportedCount(Long commentId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyLikeRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyLikeRepository.java new file mode 100644 index 0000000..3585c02 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyLikeRepository.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.domain.comment.repository; + +import com.kusitms29.backendH.domain.comment.entity.ReplyLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReplyLikeRepository extends JpaRepository { + int countByReplyId(Long replyId); + boolean existsByReplyIdAndUserId(Long replyId, Long userId); + + Optional findByReplyIdAndUserId(Long replyId, Long userId); + + List findByReplyId(Long replyId); + + void deleteAllByReplyId(Long replyId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyRepository.java new file mode 100644 index 0000000..37c4e1b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/repository/ReplyRepository.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.domain.comment.repository; + +import com.kusitms29.backendH.domain.comment.entity.Reply; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +public interface ReplyRepository extends JpaRepository { + List findByCommentId(Long commentId); + + int countByCommentId(Long commentId); + + @Modifying + @Transactional + @Query("UPDATE Reply r SET r.reported = r.reported + 1 WHERE r.id = :replyId") + void increaseReportedCount(Long replyId); + + void deleteAllByCommentId(Long commentId); + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeManager.java new file mode 100644 index 0000000..928d63d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeManager.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.repository.CommentLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentLikeManager { + private final CommentLikeRepository commentLikeRepository; + public int countByCommentId(Long commentId) { + return commentLikeRepository.countByCommentId(commentId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeModifier.java new file mode 100644 index 0000000..2de0353 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeModifier.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.CommentLike; +import com.kusitms29.backendH.domain.comment.repository.CommentLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentLikeModifier { + private final CommentLikeRepository commentLikeRepository; + + public void save(CommentLike commentLike) { + commentLikeRepository.save(commentLike); + } + + public void delete(CommentLike commentLike) { + commentLikeRepository.delete(commentLike); + } + + public void deleteAllByCommentId(Long commentId) { + commentLikeRepository.deleteAllByCommentId(commentId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeReader.java new file mode 100644 index 0000000..773cd9e --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentLikeReader.java @@ -0,0 +1,35 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.CommentLike; +import com.kusitms29.backendH.domain.comment.repository.CommentLikeRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static com.kusitms29.backendH.global.error.ErrorCode.COMMENT_LIKE_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentLikeReader { + private final CommentLikeRepository commentLikeRepository; + + public boolean existsByCommentIdAndUserId(Long commentId, Long userId) { + return commentLikeRepository.existsByCommentIdAndUserId(commentId, userId); + } + public CommentLike findByCommentIdAndUserId(Long commentId, Long userId) { + return commentLikeRepository.findByCommentIdAndUserId(commentId, userId) + .orElseThrow(() -> new EntityNotFoundException(COMMENT_LIKE_NOT_FOUND)); + } + + public List findByCommentId(Long commentId) { + return commentLikeRepository.findByCommentId(commentId); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentManager.java new file mode 100644 index 0000000..392ff99 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentManager.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentManager { + private final CommentRepository commentRepository; + public int countByPostId(Long postId) { + return commentRepository.countByPostId(postId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentModifier.java new file mode 100644 index 0000000..3b2cd52 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentModifier.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.Comment; +import com.kusitms29.backendH.domain.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentModifier { + private final CommentRepository commentRepository; + + public Comment save(Comment comment) { + return commentRepository.save(comment); + } + + public void increaseReportedCount(Long commentId) { + commentRepository.increaseReportedCount(commentId); + } + + public void delete(Comment comment) { + commentRepository.delete(comment); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentReader.java new file mode 100644 index 0000000..0b56824 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/CommentReader.java @@ -0,0 +1,29 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.Comment; +import com.kusitms29.backendH.domain.comment.repository.CommentRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.kusitms29.backendH.global.error.ErrorCode.COMMENT_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CommentReader { + private final CommentRepository commentRepository; + public List findByPostId(Long postId) { + return commentRepository.findByPostId(postId); + } + + public Comment findById(Long commentId) { + return commentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException(COMMENT_NOT_FOUND)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeManager.java new file mode 100644 index 0000000..8f5fcb4 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeManager.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.repository.ReplyLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReplyLikeManager { + private final ReplyLikeRepository replyLikeRepository; + public int countByReplyId(Long replyId) { + return replyLikeRepository.countByReplyId(replyId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeModifier.java new file mode 100644 index 0000000..176723a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeModifier.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.Comment; +import com.kusitms29.backendH.domain.comment.entity.ReplyLike; +import com.kusitms29.backendH.domain.comment.repository.ReplyLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReplyLikeModifier { + private final ReplyLikeRepository replyLikeRepository; + public void save(ReplyLike replyLike) { + replyLikeRepository.save(replyLike); + } + public void delete(ReplyLike replyLike) { + replyLikeRepository.delete(replyLike); + } + + public void deleteAllByReplyId(Long replyId) { + replyLikeRepository.deleteAllByReplyId(replyId); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeReader.java new file mode 100644 index 0000000..d3b11a2 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyLikeReader.java @@ -0,0 +1,34 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.ReplyLike; +import com.kusitms29.backendH.domain.comment.repository.ReplyLikeRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.kusitms29.backendH.global.error.ErrorCode.REPLY_LIKE_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReplyLikeReader { + private final ReplyLikeRepository replyLikeRepository; + + public List findByReplyId(Long replyId) { + return replyLikeRepository.findByReplyId(replyId); + } + + public boolean existsByReplyIdAndUserId(Long replyId, Long userId) { + return replyLikeRepository.existsByReplyIdAndUserId(replyId, userId); + } + + public ReplyLike findByReplyIdAndUserId(Long replyId, Long userId) { + return replyLikeRepository.findByReplyIdAndUserId(replyId, userId) + .orElseThrow(() -> new EntityNotFoundException(REPLY_LIKE_NOT_FOUND)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyManager.java new file mode 100644 index 0000000..6b31967 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyManager.java @@ -0,0 +1,19 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.repository.ReplyRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReplyManager { + private final ReplyRepository replyRepository; + + public int countByCommentId(Long commentId) { + return replyRepository.countByCommentId(commentId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyModifier.java new file mode 100644 index 0000000..30b02da --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyModifier.java @@ -0,0 +1,32 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.Reply; +import com.kusitms29.backendH.domain.comment.repository.ReplyRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReplyModifier { + private final ReplyRepository replyRepository; + + public Reply save(Reply reply) { + return replyRepository.save(reply); + } + + public void increaseReportedCount(Long replyId) { + replyRepository.increaseReportedCount(replyId); + } + + public void delete(Reply reply) { + replyRepository.delete(reply); + } + + public void deleteAllByCommentId(Long commentId) { + replyRepository.deleteAllByCommentId(commentId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyReader.java new file mode 100644 index 0000000..56c88c6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/comment/service/ReplyReader.java @@ -0,0 +1,29 @@ +package com.kusitms29.backendH.domain.comment.service; + +import com.kusitms29.backendH.domain.comment.entity.Reply; +import com.kusitms29.backendH.domain.comment.repository.ReplyRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.kusitms29.backendH.global.error.ErrorCode.REPLY_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReplyReader { + private final ReplyRepository replyRepository; + public Reply findById(Long replyId) { + return replyRepository.findById(replyId) + .orElseThrow(() -> new EntityNotFoundException(REPLY_NOT_FOUND)); + } + + public List findByCommentId(Long commentId) { + return replyRepository.findByCommentId(commentId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationHistory.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationHistory.java new file mode 100644 index 0000000..35b1d42 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationHistory.java @@ -0,0 +1,56 @@ +package com.kusitms29.backendH.domain.notification.entity; + +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "notification_history") +@Entity +public class NotificationHistory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_history_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String title; + + private String body; + + private String receiverToken; + + private LocalDateTime sentAt; + + private NotificationType notificationType; + + private TopCategory topCategory; + + private String infoId; + private String infoId2; + + public static NotificationHistory createHistory(User user, String title, String body, + String receiverToken, LocalDateTime sentAt, + NotificationType notificationType, TopCategory topCategory, + String infoId, String infoId2) { + return NotificationHistory.builder() + .user(user) + .title(title) + .body(body) + .receiverToken(receiverToken) + .sentAt(sentAt) + .notificationType(notificationType) + .topCategory(topCategory) + .infoId(infoId) + .infoId2(infoId2) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationSetting.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationSetting.java new file mode 100644 index 0000000..36e47ad --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationSetting.java @@ -0,0 +1,37 @@ +package com.kusitms29.backendH.domain.notification.entity; + +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "notification") +@Entity +public class NotificationSetting extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Enumerated(EnumType.STRING) + private NotificationType notificationType; + + @Enumerated(EnumType.STRING) + private Status status = Status.ACTIVE; + + public enum Status { + ACTIVE, INACTIVE; + } + + public void setStatus(Status status) { + this.status = Status.ACTIVE; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationType.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationType.java new file mode 100644 index 0000000..f4ade7d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/NotificationType.java @@ -0,0 +1,9 @@ +package com.kusitms29.backendH.domain.notification.entity; + +public enum NotificationType { + CHAT, + CHAT_ROOM_NOTICE, + SYNC_REMINDER, + COMMENT, + REVIEW +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/TopCategory.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/TopCategory.java new file mode 100644 index 0000000..62dd136 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/entity/TopCategory.java @@ -0,0 +1,25 @@ +package com.kusitms29.backendH.domain.notification.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_NOTIFICATION_TOP_CATEGORY; + +@RequiredArgsConstructor +@Getter +public enum TopCategory { + ACTIVITY("활동"), + MY_SYNC("내싱크"); + + private final String stringTopCategory; + + public static TopCategory getEnumTopCategoryFromStringTopCategory(String strTopCategory) { + return Arrays.stream(values()) + .filter(topCategory -> topCategory.stringTopCategory.equals(strTopCategory)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_NOTIFICATION_TOP_CATEGORY)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationHistoryRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationHistoryRepository.java new file mode 100644 index 0000000..6aca084 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationHistoryRepository.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.domain.notification.repository; + +import com.kusitms29.backendH.domain.notification.entity.NotificationHistory; +import com.kusitms29.backendH.domain.notification.entity.TopCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NotificationHistoryRepository extends JpaRepository { + List findByTopCategoryAndUserId(TopCategory topCategory, Long userId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..4c9eaf2 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.domain.notification.repository; + +import com.kusitms29.backendH.domain.notification.entity.NotificationSetting; +import com.kusitms29.backendH.domain.notification.entity.NotificationType; +import com.kusitms29.backendH.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface NotificationRepository extends JpaRepository { + Optional findByUserAndNotificationType(User user, NotificationType type); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/notification/service/NotificationHistoryReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/notification/service/NotificationHistoryReader.java new file mode 100644 index 0000000..c016f55 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/notification/service/NotificationHistoryReader.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.domain.notification.service; + +import com.kusitms29.backendH.domain.notification.entity.NotificationHistory; +import com.kusitms29.backendH.domain.notification.entity.TopCategory; +import com.kusitms29.backendH.domain.notification.repository.NotificationHistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class NotificationHistoryReader { + private final NotificationHistoryRepository notificationHistoryRepository; + + public List findByTopCategoryAndUserId(TopCategory topCategory, Long userId) { + return notificationHistoryRepository.findByTopCategoryAndUserId(topCategory, userId); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/Post.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/Post.java new file mode 100644 index 0000000..284240e --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/Post.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.post.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "post") +@Entity +public class Post extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + private String title; + + private String content; + @Enumerated(EnumType.STRING) + private PostType postType; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostImage.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostImage.java new file mode 100644 index 0000000..5788c3d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostImage.java @@ -0,0 +1,26 @@ +package com.kusitms29.backendH.domain.post.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "post_image") +@Entity +public class PostImage extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "image_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + private String image_url; + + private boolean isRepresentative; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostLike.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostLike.java new file mode 100644 index 0000000..40bb97d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostLike.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.post.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "postLike") +@Entity +public class PostLike extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_like_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostType.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostType.java new file mode 100644 index 0000000..3fc0e92 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/entity/PostType.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.domain.post.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_POST_TYPE; + +@RequiredArgsConstructor +@Getter +public enum PostType { + C("생활"), + Q("질문"); + private final String stringPostType; + public static PostType getEnumPostTypeFromStringPostType(String stringPostType) { + return Arrays.stream(values()) + .filter(postType -> postType.stringPostType.equals(stringPostType)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_POST_TYPE)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostImageRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostImageRepository.java new file mode 100644 index 0000000..7d8321d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostImageRepository.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.domain.post.repository; + +import com.kusitms29.backendH.domain.post.entity.PostImage; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PostImageRepository extends JpaRepository { + + List findByPostId(Long postId); + + PostImage findByPostIdAndIsRepresentative(Long postId, boolean representative); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostLikeRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostLikeRepository.java new file mode 100644 index 0000000..bb57059 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostLikeRepository.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.domain.post.repository; + +import com.kusitms29.backendH.domain.post.entity.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PostLikeRepository extends JpaRepository { + int countByPostId(Long postId); + + boolean existsByPostIdAndUserId(Long postId, Long userId); + + Optional findByPostIdAndUserId(Long postId, Long userId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..0440a49 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/repository/PostRepository.java @@ -0,0 +1,19 @@ +package com.kusitms29.backendH.domain.post.repository; + +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.entity.PostType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PostRepository extends JpaRepository { + List findByPostType(PostType postType); + + @Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword% OR p.content LIKE %:keyword%") + List searchByTitleOrContent(@Param("keyword") String keyword); + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostAppender.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostAppender.java new file mode 100644 index 0000000..05078d0 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostAppender.java @@ -0,0 +1,22 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostAppender { + private final PostRepository postRepository; + + public Post save(Post post) { + return postRepository.save(post); + } + + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageAppender.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageAppender.java new file mode 100644 index 0000000..1ba3dfb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageAppender.java @@ -0,0 +1,21 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.entity.PostImage; +import com.kusitms29.backendH.domain.post.repository.PostImageRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostImageAppender { + private final PostImageRepository postImageRepository; + + public PostImage save(PostImage postImage) { + return postImageRepository.save(postImage); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageReader.java new file mode 100644 index 0000000..3378f3e --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostImageReader.java @@ -0,0 +1,27 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.entity.PostImage; +import com.kusitms29.backendH.domain.post.repository.PostImageRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostImageReader { + private final PostImageRepository postImageRepository; + + public PostImage findByPostIdAndIsRepresentative(Long postId, boolean representative) { + return postImageRepository.findByPostIdAndIsRepresentative(postId, representative); + } + + public List findByPostId(Long postId) { + return postImageRepository.findByPostId(postId); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeAppender.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeAppender.java new file mode 100644 index 0000000..cafd1e3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeAppender.java @@ -0,0 +1,22 @@ +package com.kusitms29.backendH.domain.post.service; + + +import com.kusitms29.backendH.domain.post.entity.PostLike; +import com.kusitms29.backendH.domain.post.repository.PostLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostLikeAppender { + private final PostLikeRepository postLikeRepository; + + public PostLike save(PostLike postLike) { + return postLikeRepository.save(postLike); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeManager.java new file mode 100644 index 0000000..b7240ff --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeManager.java @@ -0,0 +1,22 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.repository.PostLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostLikeManager { + private final PostLikeRepository postLikeRepository; + public int countByPostId(Long postId) { + return postLikeRepository.countByPostId(postId); + } + + public boolean existsByPostIdAndUserId(Long postId, Long userId) { + return postLikeRepository.existsByPostIdAndUserId(postId, userId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeModifier.java new file mode 100644 index 0000000..4b3d7f6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeModifier.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.entity.PostLike; +import com.kusitms29.backendH.domain.post.repository.PostLikeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostLikeModifier { + private final PostLikeRepository postLikeRepository; + + public void delete(PostLike postLike) { + postLikeRepository.delete(postLike); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeReader.java new file mode 100644 index 0000000..f46a440 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostLikeReader.java @@ -0,0 +1,24 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.entity.PostLike; +import com.kusitms29.backendH.domain.post.repository.PostLikeRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.kusitms29.backendH.global.error.ErrorCode.POST_LIKE_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostLikeReader { + private final PostLikeRepository postLikeRepository; + + public PostLike findByPostIdAndUserId(Long postId, Long userId) { + return postLikeRepository.findByPostIdAndUserId(postId, userId) + .orElseThrow(() -> new EntityNotFoundException(POST_LIKE_NOT_FOUND)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostReader.java new file mode 100644 index 0000000..249795a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/post/service/PostReader.java @@ -0,0 +1,37 @@ +package com.kusitms29.backendH.domain.post.service; + +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.entity.PostType; +import com.kusitms29.backendH.domain.post.repository.PostRepository; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.kusitms29.backendH.global.error.ErrorCode.POST_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PostReader { + + public final PostRepository postRepository; + + public List findByPostType(PostType enumPostType) { + return postRepository.findByPostType(enumPostType); + } + + public Post findById(Long postId) { + return postRepository.findById(postId) + .orElseThrow(()-> new EntityNotFoundException(POST_NOT_FOUND)); + } + + public List searchByTitleOrContent(String keyword) { + return postRepository.searchByTitleOrContent(keyword); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/FavoriteSync.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/FavoriteSync.java new file mode 100644 index 0000000..9ae44eb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/FavoriteSync.java @@ -0,0 +1,27 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "favoriteSync") +@Entity +public class FavoriteSync extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "favorite_sync_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sync_id") + private Sync sync; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Gender.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Gender.java new file mode 100644 index 0000000..69cacaa --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Gender.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_GENDER_TYPE; + +@RequiredArgsConstructor +@Getter +public enum Gender { + + MAN("남성"), + WOMAN("여성"), + SECRET("비공개"); + + private final String stringGender; + + public static Gender getEnumFROMStringGender(String stringGender) { + return Arrays.stream(values()) + .filter(gender -> gender.stringGender.equals(stringGender)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_GENDER_TYPE)); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Language.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Language.java new file mode 100644 index 0000000..8a75118 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Language.java @@ -0,0 +1,26 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_LANGUAGE_TYPE; + +@RequiredArgsConstructor +@Getter +public enum Language { + KOREAN("korean"), + ENGLISH("english"); + + private final String stringLanguage; + + public static Language getEnumLanguageFromStringLanguage(String stringLanguage) { + return Arrays.stream(values()) + .filter(language -> language.stringLanguage.equals(stringLanguage)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_LANGUAGE_TYPE)); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Participation.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Participation.java new file mode 100644 index 0000000..0de007c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Participation.java @@ -0,0 +1,34 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "participation") +@Entity +public class Participation extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "participation_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sync_id") + private Sync sync; + + public static Participation createParticipation(User user, Sync sync) { + return Participation.builder() + .user(user) + .sync(sync) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Sync.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Sync.java new file mode 100644 index 0000000..81669be --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/Sync.java @@ -0,0 +1,99 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "sync") +@Entity +public class Sync extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "sync_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "user_id") + private User user; + //모임장소개 + private String userIntro; + //싱크소개 + private String syncIntro; + private String link; + + @Enumerated(EnumType.STRING) + private SyncType syncType; + + private String syncName; + private String image; + private String content; + private String location; + private LocalDateTime date; + //지속성에서 정기모임 + private String regularDay; + private LocalTime regularTime; + private LocalDateTime routineDate; + + //지속성 모임 : 모임 횟수 + @ColumnDefault("1") + @Builder.Default() + private int member_min = 1; + @ColumnDefault("2") + @Builder.Default() + private int member_max = 2; + + @Enumerated(EnumType.STRING) + protected Sync.Status status; + @Enumerated(EnumType.STRING) + private Type type; + private String detailType; + //일단 제휴가 뭐 없어서 일단 이렇게 함 + private String associate; + + public enum Status { + RECRUITING, COMPLETED, DELETED; + } + + public static Sync from(Long syncId) { + return new Sync(syncId,null,null,null,null,null,null,null,null,null,null,null,null,null,0,0,null,null,null,null); + } + + public static Sync createSync(User user, String userIntro, String syncIntro, SyncType syncType, + String syncName, String image, String location, LocalDateTime date, + String regularDay, LocalTime regularTime, LocalDateTime routineDate, + int member_min, int member_max, + Type type, String detailType){ + return Sync.builder() + .user(user) + .userIntro(userIntro) + .syncIntro(syncIntro) + .syncType(syncType) + .syncName(syncName) + .image(image) + .location(location) + .date(date) + .regularDay(regularDay) + .regularTime(regularTime) + .routineDate(routineDate) + .member_min(member_min) + .member_max(member_max) + .type(type) + .detailType(detailType) + .build(); + } + + public void updateNextDate(LocalDateTime routineDate) { + this.routineDate = routineDate; + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncReview.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncReview.java new file mode 100644 index 0000000..9652477 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncReview.java @@ -0,0 +1,39 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@Table(name = "syncReview") +@Entity +public class SyncReview extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "sync_review_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sync_id") + private Sync sync; + + private String content; + + public SyncReview(User user, Sync sync, String content) { + this.user = user; + this.sync = sync; + this.content = content; + } + + public static SyncReview createReview(User user, Sync sync, String content){ + return new SyncReview(user, sync, content); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncStatus.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncStatus.java new file mode 100644 index 0000000..2fe9075 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncStatus.java @@ -0,0 +1,15 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum SyncStatus { + + RECRUITING("모집중"), + COMPLETED("모집완료"), + DELETED("삭제된모임"); + + private final String syncStatus; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncType.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncType.java new file mode 100644 index 0000000..69e299d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/entity/SyncType.java @@ -0,0 +1,32 @@ +package com.kusitms29.backendH.domain.sync.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_SYNC_TYPE; + +@RequiredArgsConstructor +@Getter +public enum SyncType { + + //일회성, 지속성, 내친소 + ONETIME("일회성"), + LONGTIME("지속성"), + FROM_FRIEND("내친소"); + + private final String stringSyncType; + + public static SyncType getEnumFROMStringSyncType(String stringSyncType) { + if (stringSyncType == null) { + return null; + } + return Arrays.stream(values()) + .filter(syncType -> syncType.stringSyncType.equals(stringSyncType)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_SYNC_TYPE)); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/FavoriteSyncRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/FavoriteSyncRepository.java new file mode 100644 index 0000000..1d5b90c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/FavoriteSyncRepository.java @@ -0,0 +1,10 @@ +package com.kusitms29.backendH.domain.sync.repository; + +import com.kusitms29.backendH.domain.sync.entity.FavoriteSync; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FavoriteSyncRepository extends JpaRepository { + List findAllByUserId(Long userId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/ParticipationRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/ParticipationRepository.java new file mode 100644 index 0000000..3b13a99 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/ParticipationRepository.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.domain.sync.repository; + +import com.kusitms29.backendH.domain.sync.entity.Participation; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface ParticipationRepository extends JpaRepository { + Optional findByUserAndSync(User user, Sync sync); + List findBySync(Sync sync); + @Query("SELECT COUNT(p) FROM Participation p WHERE p.sync.id = :syncId") + int countBySyncId(@Param("syncId") Long syncId); + List findAllBySyncId(Long syncId); + List findAllByUserId(Long userId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncRepository.java new file mode 100644 index 0000000..2dacb54 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncRepository.java @@ -0,0 +1,56 @@ +package com.kusitms29.backendH.domain.sync.repository; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public interface SyncRepository extends JpaRepository { + //모임이 하루 안으로 임박한 Sync의 정보 가져오기 + @Query(value = "SELECT u.user_id, u.user_name, s.sync_name, s.sync_id, s.sync_type, s.regular_day, s.regular_time, " + + "CASE WHEN s.sync_type = 'LONGTIME' THEN s.routine_date ELSE s.date END AS effective_date " + + "FROM sync s " + + "INNER JOIN participation p ON s.sync_id = p.sync_id " + + "INNER JOIN user u ON p.user_id = u.user_id " + + "WHERE s.status != 'DELETED' " + + "AND (CASE WHEN s.sync_type = 'LONGTIME' THEN s.routine_date ELSE s.date END) IS NOT NULL " + + "AND DATE_SUB((CASE WHEN s.sync_type = 'LONGTIME' THEN s.routine_date ELSE s.date END), INTERVAL 1 DAY) <= :currentDate " + + "AND (CASE WHEN s.sync_type = 'LONGTIME' THEN s.routine_date ELSE s.date END) >= :currentDate ", + nativeQuery = true) + List> findHurrySyncInfo(LocalDateTime currentDate); + @Query("SELECT s FROM Sync s WHERE s.syncType = :syncType AND s.type = :type AND s.location = :location ORDER BY s.date DESC") + List findAllBySyncTypeWithTypeWithLocation(@Param("syncType") SyncType syncType, @Param("type") Type type, @Param("location") String location); + + @Query("SELECT s FROM Sync s WHERE s.location = :location AND s.syncType = :syncType ORDER BY s.date DESC") + List findAllByLocationAndSyncType(@Param("location") String location, @Param("syncType") SyncType syncType); + + @Query("SELECT s FROM Sync s WHERE s.location = :location AND s.type = :type ORDER BY s.date DESC") + List findAllByLocationAndType(@Param("location") String location, @Param("type") Type type); + + @Query("SELECT s FROM Sync s WHERE s.syncType = :syncType AND s.type = :type ORDER BY s.date DESC") + List findAllBySyncTypeAndType(@Param("syncType") SyncType syncType, @Param("type") Type type); + + @Query("SELECT s FROM Sync s WHERE s.location = :location ORDER BY s.date DESC") + List findAllByLocation(@Param("location") String location); + + @Query("SELECT s FROM Sync s WHERE s.syncType = :syncType ORDER BY s.date DESC") + List findAllBySyncType(@Param("syncType") SyncType syncType); + + @Query("SELECT s FROM Sync s WHERE s.type = :type ORDER BY s.date DESC") + List findAllByType(@Param("type") Type type); + @Query("SELECT s FROM Sync s WHERE s.associate IS NOT NULL AND s.associate <> '' ORDER BY s.date DESC") + List findAllByAssociateIsExistOrderByDateDesc(SyncType syncType, Type type); + @Query("SELECT s FROM Sync s WHERE s.syncType = :syncType AND s.type = :type AND s.associate IS NOT NULL AND s.associate <> '' ORDER BY s.date DESC") + List findAllBySyncTypeAndTypeAndAssociateIsExistOrderByDateDesc(SyncType syncType, Type type); + List findAll(Specification spec, Sort sort); + List findAllByLocationAndDate(String location, LocalDateTime date); + List findAllByUserId(Long userId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncReviewRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncReviewRepository.java new file mode 100644 index 0000000..4497de3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/repository/SyncReviewRepository.java @@ -0,0 +1,10 @@ +package com.kusitms29.backendH.domain.sync.repository; + +import com.kusitms29.backendH.domain.sync.entity.SyncReview; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SyncReviewRepository extends JpaRepository { + List findAllBySyncId(Long syncId); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/FavoriteSyncReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/FavoriteSyncReader.java new file mode 100644 index 0000000..7f5464b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/FavoriteSyncReader.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.sync.entity.FavoriteSync; +import com.kusitms29.backendH.domain.sync.repository.FavoriteSyncRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FavoriteSyncReader { + private final FavoriteSyncRepository favoriteSyncRepository; + + public List findAllByUserId(Long userId){ + return favoriteSyncRepository.findAllByUserId(userId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationManager.java new file mode 100644 index 0000000..709efc2 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationManager.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.sync.repository.ParticipationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ParticipationManager { + private final ParticipationRepository participationRepository; + public int countParticipationBySyncId(Long syncId){ + return participationRepository.countBySyncId(syncId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationReader.java new file mode 100644 index 0000000..ba9ce08 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/ParticipationReader.java @@ -0,0 +1,25 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.sync.entity.Participation; +import com.kusitms29.backendH.domain.sync.repository.ParticipationRepository; +import com.kusitms29.backendH.global.error.ErrorCode; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import com.kusitms29.backendH.global.error.exception.ListException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ParticipationReader { + private final ParticipationRepository participationRepository; + public List findAllBySyncId(Long syncId){ + List participationList = participationRepository.findAllBySyncId(syncId); + return ListException.throwIfEmpty(participationList, () -> new EntityNotFoundException(ErrorCode.PARTICIPATION_NOT_FOUND)); + } + public List findAllByUserId(Long userId){ + List participationList = participationRepository.findAllByUserId(userId); + return ListException.throwIfEmpty(participationList, () -> new EntityNotFoundException(ErrorCode.PARTICIPATION_NOT_FOUND)); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncAppender.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncAppender.java new file mode 100644 index 0000000..f75e169 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncAppender.java @@ -0,0 +1,15 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.sync.repository.SyncRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SyncAppender { + private final SyncRepository syncRepository; + public Sync save(Sync sync) { + return syncRepository.save(sync); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncManager.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncManager.java new file mode 100644 index 0000000..c1b9ed7 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncManager.java @@ -0,0 +1,185 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.api.sync.service.dto.response.GraphElement; +import com.kusitms29.backendH.api.sync.service.dto.response.SyncGraphResponseDto; +import com.kusitms29.backendH.domain.sync.entity.Participation; +import com.kusitms29.backendH.domain.sync.entity.Gender; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class SyncManager { + private final UserReader userReader; + private final ParticipationReader participationReader; + public Boolean validateCreateRoom(Sync sync, int count){ + if(sync.getMember_min()==count) + return true; + return false; + } + public Boolean validateJoinRoom(Sync sync, int count){ + if(sync.getMember_max()==count) + return true; + return false; + } + public SyncGraphResponseDto createGraphElementList(List participationList, String graph){ + List users = participationList.stream().map( participation -> userReader.findByUserId(participation.getUser().getId())).toList(); + if(graph.equals("participate")) + return participateGraph(users); + else if(graph.equals("gender")) + return genderGraph(users); + else if(graph.equals("university")) + return universityGraph(users); + else + return nationalGraph(users); + } + private SyncGraphResponseDto participateGraph(List users) { + Map participationCountMap = new HashMap<>(); + + for (User user : users) { + long userId = user.getId(); + List participationList = participationReader.findAllByUserId(userId); + participationCountMap.put(userId, participationList.size()); + } + + List graphElements = new ArrayList<>(); + int beginnerCount = 0; + int intermediateCount = 0; + int advancedCount = 0; + + for (int count : participationCountMap.values()) { + if (count == 1) { + beginnerCount++; + } else if (count >= 2 && count <= 3) { + intermediateCount++; + } else if (count >= 4) { + advancedCount++; + } + } + + int totalUsers = users.size(); + double beginnerPercent = (double) beginnerCount / totalUsers * 100; + double intermediatePercent = (double) intermediateCount / totalUsers * 100; + double advancedPercent = (double) advancedCount / totalUsers * 100; + + graphElements.add(GraphElement.of("처음이에요", (int) Math.round(beginnerPercent))); + graphElements.add(GraphElement.of("사용해봤어요", (int) Math.round(intermediatePercent))); + graphElements.add(GraphElement.of("고인물이에요", (int) Math.round(advancedPercent))); + + String status; + if (beginnerPercent > intermediatePercent && beginnerPercent > advancedPercent) { + status = "처음 참여해보는 멤버"; + } else if (intermediatePercent > beginnerPercent && intermediatePercent > advancedPercent) { + status = "경험 해 본 멤버"; + } else if (advancedPercent > beginnerPercent && advancedPercent > intermediatePercent) { + status = "여러 번 경험해 본 멤버"; + } else { + status = "다양한 경험을 가진 멤버들이 고르게 분포되어 있어요"; + } + + return SyncGraphResponseDto.of(graphElements, status); + } + private SyncGraphResponseDto nationalGraph(List users) { + int totalUsers = users.size(); + int koreanCount = 0; + int foreignerCount = 0; + + for (User user : users) { + if (user.getNationality().equals("한국")) { + koreanCount++; + } else { + foreignerCount++; + } + } + + double koreanPercent = (double) koreanCount / totalUsers * 100; + double foreignerPercent = (double) foreignerCount / totalUsers * 100; + + List graphElements = new ArrayList<>(); + graphElements.add(GraphElement.of("내국인", (int) Math.round(koreanPercent))); + graphElements.add(GraphElement.of("외국인", (int) Math.round(foreignerPercent))); + + String status; + if (koreanPercent < foreignerPercent) { + status = "외국인"; + } else if (koreanPercent > foreignerPercent) { + status = "내국인"; + } else { + status = "내국인과 외국인의 비율이 동일해요"; + } + + return SyncGraphResponseDto.of(graphElements, status); + } + private SyncGraphResponseDto genderGraph(List users) { + int totalUsers = users.size(); + int manCount = 0; + int womanCount = 0; + int secretCount = 0; + + for (User user : users) { + Gender gender = user.getGender(); + if (gender == Gender.MAN) { + manCount++; + } else if (gender == Gender.WOMAN) { + womanCount++; + } else if (gender == Gender.SECRET) { + secretCount++; + } + } + + double manPercent = (double) manCount / totalUsers * 100; + double womanPercent = (double) womanCount / totalUsers * 100; + double secretPercent = (double) secretCount / totalUsers * 100; + + List graphElements = new ArrayList<>(); + graphElements.add(GraphElement.of("남성", (int) Math.round(manPercent))); + graphElements.add(GraphElement.of("여성", (int) Math.round(womanPercent))); + graphElements.add(GraphElement.of("비공개", (int) Math.round(secretPercent))); + + String status = getHighestParticipationStatus(graphElements); + + return SyncGraphResponseDto.of(graphElements, status); + } + + private SyncGraphResponseDto universityGraph(List users) { + Map universityCountMap = new HashMap<>(); + + for (User user : users) { + String university = user.getUniversity(); + if (university != null) { + universityCountMap.put(university, universityCountMap.getOrDefault(university, 0) + 1); + } + } + + int totalUsers = users.size(); + List graphElements = new ArrayList<>(); + + for (Map.Entry entry : universityCountMap.entrySet()) { + String university = entry.getKey(); + int count = entry.getValue(); + double percent = (double) count / totalUsers * 100; + graphElements.add(GraphElement.of(university, (int) Math.round(percent))); + } + + String status = getHighestParticipationStatus(graphElements); + + return SyncGraphResponseDto.of(graphElements, status); + } + + private String getHighestParticipationStatus(List graphElements) { + GraphElement highestElement = graphElements.stream() + .max(Comparator.comparingInt(GraphElement::getPercent)) + .orElse(null); + + if (highestElement != null) { + return highestElement.getName(); + } else { + return "참여자가 없습니다."; + } + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReader.java new file mode 100644 index 0000000..1d01940 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReader.java @@ -0,0 +1,187 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.category.entity.Type; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import com.kusitms29.backendH.domain.sync.repository.SyncRepository; +import com.kusitms29.backendH.global.error.ErrorCode; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import com.kusitms29.backendH.global.error.exception.ListException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import jakarta.persistence.criteria.Predicate; +import org.springframework.data.domain.Sort; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SyncReader { + private final SyncRepository syncRepository; + +// public List findAllByAssociateIsExist(SyncType syncType, Type type){ +// return syncRepository.findAllBySyncTypeAndTypeAndAssociateIsExistOrderByDateDesc(syncType, type).orElseThrow(()->new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); +// } + public List findAllByLocationAndDate(String location, LocalDateTime date){ + List syncList = syncRepository.findAllByLocationAndDate(location,date); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findAllByAssociateIsExist(SyncType syncType, Type type) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + predicates.add(criteriaBuilder.isNotNull(root.get("associate"))); + predicates.add(criteriaBuilder.notEqual(root.get("associate"), "")); + + if (syncType != null) { + predicates.add(criteriaBuilder.equal(root.get("syncType"), syncType)); + } + + if (type != null) { + predicates.add(criteriaBuilder.equal(root.get("type"), type)); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + List syncList = syncRepository.findAll(spec, Sort.by(Sort.Direction.DESC, "date")); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findBySyncTypeWithTypesWithLocation(SyncType syncType, List types, String location) { + List syncList = new ArrayList<>(); + + for (Type type : types) { + try { + List sync3 = findAllBySyncTypeWithTypeWithLocation(syncType, type, location); + for (Sync sync : sync3) { + if (!syncList.contains(sync)) { + syncList.add(sync); + if (syncList.size() >= 7) { + return syncList.subList(0, 7); + } + } + } + if (!sync3.isEmpty()) { + break; + } + } catch (RuntimeException e) { + // 예외 처리 로직 추가 (예: 로깅) + // 예외를 무시하고 계속 진행 + } + } + + for (Type type : types) { + try { + List sync2 = findAllByTwoCondition(syncType, type, location); + for (Sync sync : sync2) { + if (!syncList.contains(sync)) { + syncList.add(sync); + if (syncList.size() >= 7) { + return syncList.subList(0, 7); + } + } + } + if (!sync2.isEmpty()) { + break; + } + } catch (RuntimeException e) { + // 예외 처리 로직 추가 (예: 로깅) + // 예외를 무시하고 계속 진행 + } + } + + for (Type type : types) { + try { + List sync1 = findAllByOneCondition(syncType, type, location); + for (Sync sync : sync1) { + if (!syncList.contains(sync)) { + syncList.add(sync); + if (syncList.size() >= 7) { + return syncList.subList(0, 7); + } + } + } + if (!sync1.isEmpty()) { + break; + } + } catch (RuntimeException e) { + // 예외 처리 로직 추가 (예: 로깅) + // 예외를 무시하고 계속 진행 + } + } + + return syncList; + } + public List findAllBySyncTypeWithTypeWithLocation(SyncType syncType, Type type, String location){ + List syncList = syncRepository.findAllBySyncTypeWithTypeWithLocation(syncType, type, location); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + private List findAllByTwoCondition(SyncType syncType, Type type, String location) { + List syncList = new ArrayList<>(); + + // location과 syncType이 일치하는 경우 + syncList.addAll(findAllByLocationAndSyncType(location, syncType)); + + // location과 type이 일치하는 경우 + syncList.addAll(findAllByLocationAndType(location, type)); + + // syncType과 type이 일치하는 경우 + syncList.addAll(findAllBySyncTypeAndType(syncType, type)); + + return syncList; + } + public List findAllByLocationAndSyncType(String location, SyncType syncType){ + List syncList = syncRepository.findAllByLocationAndSyncType(location,syncType); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findAllByLocationAndType(String location, Type type){ + List syncList = syncRepository.findAllByLocationAndType(location,type); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + + private List findAllByOneCondition(SyncType syncType, Type type, String location) { + List syncList = new ArrayList<>(); + + // location만 일치하는 경우 + syncList.addAll(findAllByLocation(location)); + + // syncType만 일치하는 경우 + syncList.addAll(findAllBySyncType(syncType)); + + // type만 일치하는 경우 + syncList.addAll(findAllByType(type)); + + return syncList; + } + public List findAllByLocation(String location){ + List syncList = syncRepository.findAllByLocation(location); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findAllBySyncType(SyncType syncType){ + List syncList = syncRepository.findAllBySyncType(syncType); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findAllByType(Type type){ + List syncList = syncRepository.findAllByType(type); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findAllBySyncTypeAndType(SyncType syncType, Type type){ + if(type==null) + return findAllBySyncType(syncType); + if(syncType==null) + return findAllByType(type); + List syncList = syncRepository.findAllBySyncTypeAndType(syncType, type); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public Sync findById(Long syncId){ + return syncRepository.findById(syncId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } + public List findAllByUserId(Long userId){ + List syncList = syncRepository.findAllByUserId(userId); + return ListException.throwIfEmpty(syncList, () -> new EntityNotFoundException(ErrorCode.SYNC_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewAppender.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewAppender.java new file mode 100644 index 0000000..bd288d9 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewAppender.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.sync.entity.SyncReview; +import com.kusitms29.backendH.domain.sync.repository.SyncReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class SyncReviewAppender { + private final SyncReviewRepository syncReviewRepository; + @Transactional + public SyncReview createReview(SyncReview syncReview){ + return syncReviewRepository.save(syncReview); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewReader.java new file mode 100644 index 0000000..fdc20fa --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/sync/service/SyncReviewReader.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.domain.sync.service; + +import com.kusitms29.backendH.domain.sync.entity.SyncReview; +import com.kusitms29.backendH.domain.sync.repository.SyncReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SyncReviewReader { + private final SyncReviewRepository syncReviewRepository; + public List findAllBySyncId(Long syncId){ + return syncReviewRepository.findAllBySyncId(syncId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/PlatformUserInfo.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/PlatformUserInfo.java new file mode 100644 index 0000000..f492db1 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/PlatformUserInfo.java @@ -0,0 +1,22 @@ +package com.kusitms29.backendH.domain.user.auth; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class PlatformUserInfo { + private String id; + private String email; + private String name; + private String picture; + + public static PlatformUserInfo createPlatformUserInfo(String id, String email, String name, String picture) { + return PlatformUserInfo.builder() + .id(id) + .email(email) + .name(name) + .picture(picture) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/RestTemplateProvider.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/RestTemplateProvider.java new file mode 100644 index 0000000..9b53332 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/RestTemplateProvider.java @@ -0,0 +1,62 @@ +package com.kusitms29.backendH.domain.user.auth; + +import com.kusitms29.backendH.domain.user.auth.google.GoogleAuthProvider; +import com.kusitms29.backendH.domain.user.auth.google.GoogleUserInfo; +import com.kusitms29.backendH.domain.user.auth.kakao.KakaoAuthProvider; +import com.kusitms29.backendH.domain.user.auth.kakao.KakaoUserInfo; +import com.kusitms29.backendH.domain.user.entity.Platform; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RestTemplateProvider { + private final GoogleAuthProvider googleAuthProvider; + private final KakaoAuthProvider kakaoAuthProvider; + + public PlatformUserInfo getUserInfoUsingRestTemplate(Platform platform, String accessToken) { + ResponseEntity platformResponse = getUserInfoFromPlatform(platform, accessToken); + return getUserInfoFromPlatformInfo(platform, platformResponse.getBody()); + } + + private ResponseEntity getUserInfoFromPlatform(Platform platform, String accessToken) { + if (platform.equals(Platform.KAKAO)) + return kakaoAuthProvider.createGetRequest(accessToken); + return googleAuthProvider.createGetRequest(accessToken); + } + + private PlatformUserInfo getUserInfoFromPlatformInfo(Platform platform, String platformInfo) { + if (platform.equals(Platform.KAKAO)) { + KakaoUserInfo kakaoUserInfo = kakaoAuthProvider.getKakaoUserInfoFromPlatformInfo(platformInfo); + return PlatformUserInfo.createPlatformUserInfo( + Long.toString(kakaoUserInfo.getId()), + kakaoUserInfo.getKakaoAccount().getEmail(), + getNickName(kakaoUserInfo), + getPicture(kakaoUserInfo)); + } else { + GoogleUserInfo googleUserInfo = googleAuthProvider.getGoogleUserInfoFromPlatformInfo(platformInfo); + return PlatformUserInfo.createPlatformUserInfo( + googleUserInfo.getId(), + googleUserInfo.getEmail(), + googleUserInfo.getName(), + googleUserInfo.getPicture()); + } + } + + private String getNickName(KakaoUserInfo kakaoUserInfo) { + if (kakaoUserInfo.getProperties() != null) { + return kakaoUserInfo.getProperties().getNickname(); + } else { + return "Unknown"; + } + } + + private String getPicture(KakaoUserInfo kakaoUserInfo) { + if (kakaoUserInfo.getProperties() != null) { + return kakaoUserInfo.getProperties().getProfileImage(); + } else { + return "Unknown"; + } + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleAuthProvider.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleAuthProvider.java new file mode 100644 index 0000000..980b2b5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleAuthProvider.java @@ -0,0 +1,49 @@ +package com.kusitms29.backendH.domain.user.auth.google; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kusitms29.backendH.global.error.exception.InternalServerException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import static com.kusitms29.backendH.domain.user.auth.google.GoogleToken.createGoogleToken; +import static com.kusitms29.backendH.global.error.ErrorCode.JSON_PARSING_ERROR; + + +@RequiredArgsConstructor +@Component +public class GoogleAuthProvider { + private static final String GOOGLE_URL = "https://www.googleapis.com/oauth2/v1/userinfo"; + private static final String HEADER_TYPE = "Authorization"; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + public ResponseEntity createGetRequest(String accessToken) { + GoogleToken googleToken = createGoogleToken(accessToken); + String googleAccessToken = googleToken.getAccessTokenWithTokenType(); + HttpEntity request = createHttpEntityFromGoogleToken(googleAccessToken); + return restTemplate.exchange(GOOGLE_URL, HttpMethod.GET, request, String.class); + } + + public GoogleUserInfo getGoogleUserInfoFromPlatformInfo(String platformInfo) { + GoogleUserInfo googleUserInfo; + try { + googleUserInfo = objectMapper.readValue(platformInfo, GoogleUserInfo.class); + } catch (JsonProcessingException e) { + throw new InternalServerException(JSON_PARSING_ERROR); + } + return googleUserInfo; + } + + private HttpEntity createHttpEntityFromGoogleToken(String googleAccessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HEADER_TYPE, googleAccessToken); + return new HttpEntity<>(headers); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleToken.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleToken.java new file mode 100644 index 0000000..133bbce --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleToken.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.domain.user.auth.google; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class GoogleToken { + private static final String TOKEN_TYPE = "Bearer "; + private String accessToken; + + public static GoogleToken createGoogleToken(String authToken) { + return new GoogleToken(authToken); + } + + public String getAccessTokenWithTokenType() { + return TOKEN_TYPE + accessToken; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleUserInfo.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleUserInfo.java new file mode 100644 index 0000000..559df39 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/google/GoogleUserInfo.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.domain.user.auth.google; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class GoogleUserInfo { + private String id; + private String email; + private Boolean verifiedEmail; + private String name; + private String givenName; + private String familyName; + private String picture; + private String locale; + +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoAuthProvider.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoAuthProvider.java new file mode 100644 index 0000000..b9bd5fb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoAuthProvider.java @@ -0,0 +1,48 @@ +package com.kusitms29.backendH.domain.user.auth.kakao; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kusitms29.backendH.global.error.exception.InternalServerException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import static com.kusitms29.backendH.domain.user.auth.kakao.KakaoToken.createKakaoToken; +import static com.kusitms29.backendH.global.error.ErrorCode.JSON_PARSING_ERROR; + + +@RequiredArgsConstructor +@Component +public class KakaoAuthProvider { + private final static String KAKAO_URL = "https://kapi.kakao.com/v2/user/me"; + private static final String HEADER_TYPE = "Authorization"; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + public ResponseEntity createGetRequest(String accessToken) { + KakaoToken kakaoToken = createKakaoToken(accessToken); + String kakaoAccessToken = kakaoToken.getAccessTokenWithTokenType(); + HttpEntity request = createHttpEntityFromKakaoToken(kakaoAccessToken); + return restTemplate.exchange(KAKAO_URL, HttpMethod.GET, request, String.class); + } + + public KakaoUserInfo getKakaoUserInfoFromPlatformInfo(String platformInfo) { + KakaoUserInfo kakaoUserInfo; + try { + kakaoUserInfo = objectMapper.readValue(platformInfo, KakaoUserInfo.class); + } catch (JsonProcessingException e) { + throw new InternalServerException(JSON_PARSING_ERROR); + } + return kakaoUserInfo; + } + + private HttpEntity createHttpEntityFromKakaoToken(String kakaoAccessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HEADER_TYPE, kakaoAccessToken); + return new HttpEntity<>(headers); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoToken.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoToken.java new file mode 100644 index 0000000..7d191f5 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoToken.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.domain.user.auth.kakao; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class KakaoToken { + private static final String TOKEN_TYPE = "Bearer "; + private String accessToken; + + public static KakaoToken createKakaoToken(String authToken) { + return new KakaoToken(authToken); + } + + public String getAccessTokenWithTokenType() { + return TOKEN_TYPE + accessToken; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoUserInfo.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoUserInfo.java new file mode 100644 index 0000000..670ef55 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/auth/kakao/KakaoUserInfo.java @@ -0,0 +1,29 @@ +package com.kusitms29.backendH.domain.user.auth.kakao; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +public class KakaoUserInfo { + private Long id; + private KakaoAccount kakaoAccount; + private Properties properties; + + @Getter + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) + public static class KakaoAccount { + private boolean hasEmail; + private String email; + } + + @Getter + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) + public static class Properties { + private String nickname; + private String profileImage; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Coupon.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Coupon.java new file mode 100644 index 0000000..90ccdc2 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Coupon.java @@ -0,0 +1,29 @@ +package com.kusitms29.backendH.domain.user.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "coupon") +@Entity +public class Coupon extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "coupon_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String name; + private String content; + private LocalDate expired_date; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Platform.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Platform.java new file mode 100644 index 0000000..1f097ef --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/Platform.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.domain.user.entity; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_PLATFORM_TYPE; + + +@RequiredArgsConstructor +@Getter +public enum Platform { + GOOGLE("google"), + KAKAO("kakao"), + WITHDRAW("withdraw"); + + private final String stringPlatform; + + public static Platform getEnumPlatformFromStringPlatform(String stringPlatform) { + return Arrays.stream(values()) + .filter(platform -> platform.stringPlatform.equals(stringPlatform)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_PLATFORM_TYPE)); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/RefreshToken.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/RefreshToken.java new file mode 100644 index 0000000..92f03d7 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/RefreshToken.java @@ -0,0 +1,25 @@ +package com.kusitms29.backendH.domain.user.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@AllArgsConstructor +@Builder +@Getter +@RedisHash(value = "refreshToken", timeToLive = 604800000) +public class RefreshToken { + @Id + private Long id; + private String refreshToken; + + public static RefreshToken createRefreshToken(Long userId, String refreshToken) { + return RefreshToken.builder() + .id(userId) + .refreshToken(refreshToken) + .build(); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/User.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/User.java new file mode 100644 index 0000000..3d19a53 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/entity/User.java @@ -0,0 +1,89 @@ +package com.kusitms29.backendH.domain.user.entity; + +import com.kusitms29.backendH.global.common.BaseEntity; +import com.kusitms29.backendH.domain.sync.entity.Gender; +import com.kusitms29.backendH.domain.sync.entity.Language; +import com.kusitms29.backendH.domain.sync.entity.SyncType; +import com.kusitms29.backendH.domain.user.auth.PlatformUserInfo; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Setter +@Table(name = "user") +@Entity +public class User extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long id; + @Enumerated(EnumType.STRING) + private Platform platform; + @Column(unique = true) + private String platformId; + private String email; + private String userName; + private String profile; + private String refreshToken; + private String sessionId; + + @Enumerated(EnumType.STRING) + private Language language; + private String university; + private String nationality; + @Enumerated(EnumType.STRING) + private Gender gender; + + //일회성, 지속성, 내친소 + @Enumerated(EnumType.STRING) + private SyncType syncType; + + private String location; + + private String languageLevel; + + public static User createUser(PlatformUserInfo platformUserInfo, Platform platform, String sessionId) { + return User.builder() + .platformId(platformUserInfo.getId()) + .platform(platform) + .email(platformUserInfo.getEmail()) + .userName(platformUserInfo.getName()) + .profile(platformUserInfo.getPicture()) + .sessionId(sessionId) + .build(); + } + public static User from(Long userId) { + return new User(userId,null,null,null,null,null,null,null,null,null,null,null,null,null,null); + } + + public void updateOnBoardingWithoutCategory(String language, String profileImage, String userName, String countryName, String gender, + String university, String email, String sycnType) { + this.setLanguage(Language.valueOf(language)); + this.setProfile(profileImage); + this.setUserName(userName); + this.setNationality(countryName); + this.setGender(Gender.valueOf(gender)); + this.setUniversity(university); + this.setEmail(email); + this.setSyncType(SyncType.valueOf(sycnType)); + } + public void updateProfile(String profile, String name, Gender gender, SyncType sycnType){ + this.profile = profile; + this.userName = name; + this.gender = gender; + this.syncType = sycnType; + } + + public void updatePlatform(Platform platform) { + this.platform = platform; + } + + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/ip/IpService.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/ip/IpService.java new file mode 100644 index 0000000..f1965a7 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/ip/IpService.java @@ -0,0 +1,41 @@ +package com.kusitms29.backendH.domain.user.ip; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Service; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +@Service +public class IpService { + public String getClientIpAddress(HttpServletRequest request) { + String[] headerTypes = {"X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"}; + String ip = null; + + for (String headerType : headerTypes) { + ip = request.getHeader(headerType); + if (ip != null) { + break; + } + } + + if (ip == null) { + ip = request.getRemoteAddr(); + } + + // IPv6 주소를 IPv4 주소로 변환 + if (ip != null && ip.indexOf(':') != -1) { + try { + InetAddress inetAddress = InetAddress.getByName(ip); + if (inetAddress instanceof Inet4Address) { + ip = inetAddress.getHostAddress(); + } + } catch (UnknownHostException e) { + // 변환 실패 시 원래의 IP 주소 반환 + } + } + + return ip; + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/repository/RefreshTokenRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..94de360 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/repository/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package com.kusitms29.backendH.domain.user.repository; + + +import com.kusitms29.backendH.domain.user.entity.RefreshToken; +import org.springframework.data.repository.CrudRepository; + +public interface RefreshTokenRepository extends CrudRepository { +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/repository/UserRepository.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..0ab03b8 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.domain.user.repository; + + +import com.kusitms29.backendH.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByPlatformId(String platformId); + Optional findByEmail(String email); +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserModifier.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserModifier.java new file mode 100644 index 0000000..5696941 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserModifier.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.domain.user.service; + +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserModifier { + private final UserRepository userRepository; + + public User save(User user) { + return userRepository.save(user); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserReader.java b/Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserReader.java new file mode 100644 index 0000000..b1a40ca --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/domain/user/service/UserReader.java @@ -0,0 +1,24 @@ +package com.kusitms29.backendH.domain.user.service; + +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.repository.UserRepository; +import com.kusitms29.backendH.global.error.ErrorCode; +import com.kusitms29.backendH.global.error.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserReader { + private final UserRepository userRepository; + + public User findByUserId(Long userId){ + return userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND)); + } + + public Optional findByPlatformId(String platformId) { + return userRepository.findByPlatformId(platformId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/common/BaseEntity.java b/Main/src/main/java/com/kusitms29/backendH/global/common/BaseEntity.java new file mode 100644 index 0000000..7d0019f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/common/BaseEntity.java @@ -0,0 +1,27 @@ +package com.kusitms29.backendH.global.common; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@Getter +@Setter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/common/HealthCheckApiController.java b/Main/src/main/java/com/kusitms29/backendH/global/common/HealthCheckApiController.java new file mode 100644 index 0000000..4eab610 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/common/HealthCheckApiController.java @@ -0,0 +1,141 @@ +package com.kusitms29.backendH.global.common; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kusitms29.backendH.infra.config.auth.UserId; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class HealthCheckApiController { + private static final String GOOGLE_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"; + private static final String GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"; + private static final String KAKAO_AUTH_ENDPOINT = "https://kauth.kakao.com/oauth/authorize"; + private static final String KAKAO_TOKEN_ENDPOINT = "https://kauth.kakao.com/oauth/token"; + + @Value("${app.google.client.id}") + private String GOOGLE_CLIENT_ID; + + @Value("${app.google.client.secret}") + private String GOOGLE_CLIENT_SECRET; + + @Value("${app.google.callback.url}") + private String GOOGLE_REDIRECT_URI; + + @Value("${app.kakao.client.id}") + private String KAKAO_CLIENT_ID; + + @Value("${app.kakao.client.secret}") + private String KAKAO_CLIENT_SECRET; + + @Value("${app.kakao.callback.url}") + private String KAKAO_REDIRECT_URI; + @GetMapping("google") + public ResponseEntity googleOauth(HttpServletRequest request) throws IOException { + String code = extractCode(request); + + if (code == null) { + String authUrl = GOOGLE_AUTH_ENDPOINT + + "?client_id=" + GOOGLE_CLIENT_ID + + "&redirect_uri=" + GOOGLE_REDIRECT_URI + + "&response_type=code" + + "&scope=email%20profile"; + + return ResponseEntity.status(HttpStatus.FOUND).header(HttpHeaders.LOCATION, authUrl).build(); + } else { + RestTemplate restTemplate = new RestTemplate(); + String accessToken = restTemplate.postForObject(GOOGLE_TOKEN_ENDPOINT + + "?client_id=" + GOOGLE_CLIENT_ID + + "&client_secret=" + GOOGLE_CLIENT_SECRET + + "&redirect_uri=" + GOOGLE_REDIRECT_URI + + "&code=" + code + + "&grant_type=authorization_code", null, String.class); + + // Access Token을 이용한 추가 처리 로직 작성 + // ... + + return new ResponseEntity<>(accessToken, HttpStatus.OK); + } + } + + private String extractCode(HttpServletRequest request) { + String fullUrl = request.getRequestURL().toString(); + String queryString = request.getQueryString(); + + if (queryString != null && queryString.contains("code=")) { + return queryString.split("code=")[1].split("&")[0]; + } + + return null; + } + @GetMapping("/oauth/google") + public void googleOauth(HttpServletResponse response) throws IOException { + String authUrl = GOOGLE_AUTH_ENDPOINT + + "?client_id=" + GOOGLE_CLIENT_ID + + "&redirect_uri=" + GOOGLE_REDIRECT_URI + + "&response_type=code" + + "&scope=email%20profile"; + + response.sendRedirect(authUrl); + } + + @GetMapping("/oauth/google/callback") + public ResponseEntity googleOauthCallback(@RequestParam(name = "code") String code) { + RestTemplate restTemplate = new RestTemplate(); + String accessToken = restTemplate.postForObject(GOOGLE_TOKEN_ENDPOINT + + "?client_id=" + GOOGLE_CLIENT_ID + + "&client_secret=" + GOOGLE_CLIENT_SECRET + + "&redirect_uri=" + GOOGLE_REDIRECT_URI + + "&code=" + code + + "&grant_type=authorization_code", null, String.class); + + // Access Token을 이용한 추가 처리 로직 작성 + // ... + + return new ResponseEntity<>(accessToken, HttpStatus.OK); + } + + @GetMapping("/oauth/kakao") + public void kakaoOauth(HttpServletResponse response) throws IOException { + String authUrl = KAKAO_AUTH_ENDPOINT + + "?client_id=" + KAKAO_CLIENT_ID + + "&redirect_uri=" + KAKAO_REDIRECT_URI + + "&response_type=code"; + + response.sendRedirect(authUrl); + } + + @GetMapping("/oauth/kakao/callback") + public ResponseEntity kakaoOauthCallback(@RequestParam(name = "code") String code) { + RestTemplate restTemplate = new RestTemplate(); + String accessToken = restTemplate.postForObject(KAKAO_TOKEN_ENDPOINT + + "?grant_type=authorization_code" + + "&client_id=" + KAKAO_CLIENT_ID + + "&client_secret=" + KAKAO_CLIENT_SECRET + + "&redirect_uri=" + KAKAO_REDIRECT_URI + + "&code=" + code, null, String.class); + + // Access Token을 이용한 추가 처리 로직 작성 + // ... + + return new ResponseEntity<>(accessToken, HttpStatus.OK); + } + @RequestMapping("/") + public Long MeetUpServer(@UserId Long userId) { + return userId; + } + +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/global/common/SuccessCode.java b/Main/src/main/java/com/kusitms29/backendH/global/common/SuccessCode.java new file mode 100644 index 0000000..01a06c1 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/common/SuccessCode.java @@ -0,0 +1,24 @@ +package com.kusitms29.backendH.global.common; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum SuccessCode { + /** + * 200 Ok + */ + OK(HttpStatus.OK, "요청이 성공했습니다."), + + /** + * 201 Created + */ + CREATED(HttpStatus.CREATED, "요청이 성공했습니다."); + + private final HttpStatus httpStatus; + private final String message; +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/global/common/SuccessResponse.java b/Main/src/main/java/com/kusitms29/backendH/global/common/SuccessResponse.java new file mode 100644 index 0000000..d94560b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/common/SuccessResponse.java @@ -0,0 +1,37 @@ +package com.kusitms29.backendH.global.common; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class SuccessResponse { + private int status; + private String message; + private T data; + + public static ResponseEntity> ok(T data) { + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(SuccessCode.OK, data)); + } + + public static ResponseEntity> created(T data) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(SuccessResponse.of(SuccessCode.CREATED, data)); + } + + + public static SuccessResponse of(SuccessCode successCode, T data) { + return SuccessResponse.builder() + .status(successCode.getHttpStatus().value()) + .message(successCode.getMessage()) + .data(data) + .build(); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/common/TimeCalculator.java b/Main/src/main/java/com/kusitms29/backendH/global/common/TimeCalculator.java new file mode 100644 index 0000000..e30225c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/common/TimeCalculator.java @@ -0,0 +1,76 @@ +package com.kusitms29.backendH.global.common; + +import lombok.extern.slf4j.Slf4j; + +import java.time.*; +import java.time.temporal.TemporalAdjusters; + +@Slf4j +public class TimeCalculator { + public static String calculateTimeDifference(LocalDateTime date) { + log.info("date :: {}", date); + LocalDateTime now = LocalDateTime.now(); + log.info("now :: {}", now); + Duration duration = Duration.between(date, now); + + long minutes = duration.toMinutes(); + if (minutes < 1) { + return "방금 전"; + } + + if (minutes < 60) { + return minutes + "분 전"; + } + + long hours = duration.toHours(); + if (hours < 24) { + return hours + "시간 전"; + } + + long days = duration.toDays(); + if (days < 7) { + return days + "일 전"; + } + + long weeks = days / 7; + if (weeks < 4) { + return weeks + "주 전"; + } + + long months = days / 30; + if (months < 12) { + return months + "달 전"; + } + + long years = months / 12; + return years + "년 전"; + } + + public static DayOfWeek convertStringToDayOfWeek(String day) { + switch (day) { + case "월": + return DayOfWeek.MONDAY; + case "화": + return DayOfWeek.TUESDAY; + case "수": + return DayOfWeek.WEDNESDAY; + case "목": + return DayOfWeek.THURSDAY; + case "금": + return DayOfWeek.FRIDAY; + case "토": + return DayOfWeek.SATURDAY; + case "일": + return DayOfWeek.SUNDAY; + default: + throw new IllegalArgumentException("Invalid day of the week: " + day); + } + } + + public static LocalDateTime getNextWeekDate(DayOfWeek dayOfWeek) { + LocalDate today = LocalDate.now(); + LocalDate nextWeekDate = today.with(TemporalAdjusters.next(dayOfWeek)); + return LocalDateTime.of(nextWeekDate, LocalTime.MIDNIGHT); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/common/TimeConverter.java b/Main/src/main/java/com/kusitms29/backendH/global/common/TimeConverter.java new file mode 100644 index 0000000..e5ab9a1 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/common/TimeConverter.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.global.common; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public interface TimeConverter { + static LocalDate convertToLocalDate(LocalDateTime localDateTime) { + return localDateTime.toLocalDate(); + } + + static LocalTime convertToLocalTime(LocalDateTime localDateTime) { + return localDateTime.toLocalTime(); + } + + static LocalDateTime convertToLocalDateTime(LocalDate localDate, LocalTime localTime) { + return LocalDateTime.of(localDate, localTime); + } + + static LocalDateTime convertToStartLocalDateTime(LocalDate localDate) { + return localDate.atStartOfDay(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/ErrorCode.java b/Main/src/main/java/com/kusitms29/backendH/global/error/ErrorCode.java new file mode 100644 index 0000000..1aec827 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/ErrorCode.java @@ -0,0 +1,110 @@ +package com.kusitms29.backendH.global.error; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum ErrorCode { + /** + * 400 Bad Request + */ + BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + INVALID_USER_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 유저 타입니다."), + INVALID_PLATFORM_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 플랫폼입니다"), + INVALID_IMAGE_TYPE(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일입니다."), + INVALID_SYNC_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 싱크타입입니다."), + + INVALID_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 관심사입니다."), + INVALID_LANGUAGE_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 언어타입입니다."), + INVALID_PARENT_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 카테고리부모타입입니다."), + INVALID_GENDER_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 성별타입입니다."), + INVALID_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 알림 타입입니다."), + INVALID_NOTIFICATION_TOP_CATEGORY(HttpStatus.BAD_REQUEST, "유효하지 않은 알림 탑카테고리입니다."), + INVALID_UNIVERSITY_NAME(HttpStatus.BAD_REQUEST, "유효하지 않은 대학이름입니다."), + INVALID_UNIVERSITY_DOMAIN(HttpStatus.BAD_REQUEST, "대학과 일치하지 않는 메일 도메인입니다."), + INVALID_POST_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 게시물타입입니다."), + INVALID_PARENT_CHILD_CATEGORY(HttpStatus.BAD_REQUEST, "부모 카테고리가 불일치합니다."), + + + + /** + * 401 Unauthorized + */ + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "리소스 접근 권한이 없습니다."), + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요."), + INVALID_ACCESS_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "액세스 토큰의 값이 올바르지 않습니다."), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰이 만료되었습니다. 재발급 받아주세요."), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 형식이 올바르지 않습니다."), + INVALID_REFRESH_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 값이 올바르지 않습니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다. 다시 로그인해 주세요."), + NOT_MATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "일치하지 않는 리프레시 토큰입니다."), + INVALID_AUTH_CODE(HttpStatus.UNAUTHORIZED, "인증을 실패했습니다."), + + + /** + * 403 Forbidden + */ + FORBIDDEN(HttpStatus.FORBIDDEN, "리소스 접근 권한이 없습니다."), + + /** + * 404 Not Found + */ + ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "엔티티를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 카테고리입니다."), + SYNC_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 싱크입니다."), + FCMTOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "FCM 토큰을 찾을 수 없습니다."), + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "알림을 찾을 수 없습니다."), + PARTICIPATION_NOT_FOUND(HttpStatus.NOT_FOUND, "참여자를 찾을 수 없습니다."), + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "게시물을 찾을 수 없습니다."), + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."), + POST_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "게시물좋아요를 찾을 수 없습니다."), + COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글좋아요를 찾을 수 없습니다."), + REPLY_NOT_FOUND(HttpStatus.NOT_FOUND, "대댓글을 찾을 수 없습니다."), + REPLY_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "대댓글좋아요를 찾을 수 없습니다."), + + /** + * 405 Method Not Allowed + */ + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "잘못된 HTTP method 요청입니다."), + PARTICIPATION_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "최대 인원 수가 모두 채워진 싱크입니다."), + TOO_LONG_TITLE_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "제목은 30자까지만 작성할 수 있어요"), + TOO_LONG_CONTENT_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "내용은 300자까지만 작성할 수 있어요"), + TOO_LONG_COMMENT_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "댓글은 30자까지만 작성할 수 있어요"), + TOO_MANY_IMAGES_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "이미지는 최대 5개까지 입니다."), + SYNC_NAME_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "싱크 제목은 15자까지만 작성할 수 있어요"), + SYNC_INTRO_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "싱크 내용은 500자까지만 작성할 수 있어요."), + USER_INTRO_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "싱크장 소개는 50자까지만 작성할 수 있어요,"), + SYNC_MIN_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "최소인원은 3명부터입니다."), + SYNC_MAX_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "최대인원은 30명까지입니다."), + + /** + * 409 Conflict + */ + CONFLICT(HttpStatus.CONFLICT, "이미 존재하는 리소스입니다."), + DUPLICATE_USER(HttpStatus.CONFLICT, "이미 존재하는 회원입니다."), + DUPLICATE_TEAM(HttpStatus.CONFLICT, "이미 존재하는 팀입니다."), + DUPLICATE_PARTICIPATION(HttpStatus.CONFLICT, "이미 참여했습니다."), + DUPLICATE_SCHOOL_MAIL(HttpStatus.CONFLICT, "이미 메일을 보냈습니다."), + DUPLICATE_POST_LIKE(HttpStatus.CONFLICT, "이미 게시글을 좋아요했습니다."), + DUPLICATE_COMMENT_LIKE(HttpStatus.CONFLICT, "이미 댓글을 좋아요했습니다."), + DUPLICATE_REPLY_LIKE(HttpStatus.CONFLICT, "이미 대댓글을 좋아요했습니다."), + + /** + * 500 Internal Server Error + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), + JSON_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Json 으로 변환할 수 없는 String 입니다."), + S3_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다."), + MAIL_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "메일 전송에 실패했습니다."), + UNIVERSITY_API_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "대학 검증 API요청을 실패했습니다."), + PAPAGO_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파파고 API요청을 실패했습니다."), + SEOUL_ADDRESS_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서울 주소 요청을 실패했습니다."); + + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/dto/ErrorBaseResponse.java b/Main/src/main/java/com/kusitms29/backendH/global/error/dto/ErrorBaseResponse.java new file mode 100644 index 0000000..c368d94 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/dto/ErrorBaseResponse.java @@ -0,0 +1,23 @@ +package com.kusitms29.backendH.global.error.dto; + +import com.kusitms29.backendH.global.error.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class ErrorBaseResponse { + private int status; + private String message; + + public static ErrorBaseResponse of(ErrorCode errorCode) { + return ErrorBaseResponse.builder() + .status(errorCode.getHttpStatus().value()) + .message(errorCode.getMessage()) + .build(); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/BusinessException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/BusinessException.java new file mode 100644 index 0000000..6a65fc0 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/BusinessException.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ConflictException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ConflictException.java new file mode 100644 index 0000000..5f904c9 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ConflictException.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class ConflictException extends BusinessException { + public ConflictException() { + super(ErrorCode.CONFLICT); + } + + public ConflictException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/EntityNotFoundException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/EntityNotFoundException.java new file mode 100644 index 0000000..33ea558 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/EntityNotFoundException.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class EntityNotFoundException extends BusinessException { + public EntityNotFoundException() { + super(ErrorCode.ENTITY_NOT_FOUND); + } + + public EntityNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ForbiddenException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ForbiddenException.java new file mode 100644 index 0000000..69bdb9b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ForbiddenException.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class ForbiddenException extends BusinessException { + public ForbiddenException() { + super(ErrorCode.FORBIDDEN); + } + + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/InternalServerException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/InternalServerException.java new file mode 100644 index 0000000..aa82cb3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/InternalServerException.java @@ -0,0 +1,10 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class InternalServerException extends BusinessException { + public InternalServerException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/InvalidValueException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/InvalidValueException.java new file mode 100644 index 0000000..a9efd04 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/InvalidValueException.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class InvalidValueException extends BusinessException { + public InvalidValueException() { + super(ErrorCode.BAD_REQUEST); + } + + public InvalidValueException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ListException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ListException.java new file mode 100644 index 0000000..3230bc0 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/ListException.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH.global.error.exception; + +import java.util.List; +import java.util.function.Supplier; + +public class ListException { + public static List throwIfEmpty(List list, Supplier exceptionSupplier) { + if (list.isEmpty()) { + throw exceptionSupplier.get(); + } + return list; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/NotAllowedException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/NotAllowedException.java new file mode 100644 index 0000000..5f42217 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/NotAllowedException.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.global.error.exception; + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class NotAllowedException extends BusinessException { + + public NotAllowedException() { + super(ErrorCode.METHOD_NOT_ALLOWED); + } + + public NotAllowedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/exception/UnauthorizedException.java b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/UnauthorizedException.java new file mode 100644 index 0000000..e0bace0 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/exception/UnauthorizedException.java @@ -0,0 +1,14 @@ +package com.kusitms29.backendH.global.error.exception; + + +import com.kusitms29.backendH.global.error.ErrorCode; + +public class UnauthorizedException extends BusinessException { + public UnauthorizedException() { + super(ErrorCode.UNAUTHORIZED); + } + + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/global/error/handler/GlobalExceptionHandler.java b/Main/src/main/java/com/kusitms29/backendH/global/error/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..9e686e9 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/global/error/handler/GlobalExceptionHandler.java @@ -0,0 +1,80 @@ +package com.kusitms29.backendH.global.error.handler; + + +import com.kusitms29.backendH.global.error.ErrorCode; +import com.kusitms29.backendH.global.error.dto.ErrorBaseResponse; +import com.kusitms29.backendH.global.error.exception.BusinessException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + /** + * Valid & Validated annotation의 binding error를 handling합니다. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error(">>> handle: MethodArgumentNotValidException ", e); + final ErrorBaseResponse errorBaseResponse = ErrorBaseResponse.of(ErrorCode.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorBaseResponse); + } + + /** + * ModelAttribute annotation의 binding error를 handling합니다. + */ + @ExceptionHandler(BindException.class) + protected ResponseEntity handleBindException(BindException e) { + log.error(">>> handle: BindException ", e); + final ErrorBaseResponse errorBaseResponse = ErrorBaseResponse.of(ErrorCode.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorBaseResponse); + } + + /** + * RequestParam annotation의 binding error를 handling합니다. + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + log.error(">>> handle: MethodArgumentTypeMismatchException ", e); + final ErrorBaseResponse errorBaseResponse = ErrorBaseResponse.of(ErrorCode.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorBaseResponse); + } + + /** + * 지원하지 않는 HTTP method로 요청 시 발생하는 error를 handling합니다. + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + log.error(">>> handle: HttpRequestMethodNotSupportedException ", e); + final ErrorBaseResponse errorBaseResponse = ErrorBaseResponse.of(ErrorCode.METHOD_NOT_ALLOWED); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(errorBaseResponse); + } + + /** + * BusinessException을 handling합니다. + */ + @ExceptionHandler(BusinessException.class) + protected ResponseEntity handleBusinessException(final BusinessException e) { + log.error(">>> handle: BusinessException ", e); + final ErrorCode errorCode = e.getErrorCode(); + final ErrorBaseResponse errorBaseResponse = ErrorBaseResponse.of(errorCode); + return ResponseEntity.status(errorCode.getHttpStatus()).body(errorBaseResponse); + } + + /** + * 위에서 정의한 Exception을 제외한 모든 예외를 handling합니다. + */ + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(Exception e) { + log.error(">>> handle: Exception ", e); + final ErrorBaseResponse errorBaseResponse = ErrorBaseResponse.of(ErrorCode.INTERNAL_SERVER_ERROR); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorBaseResponse); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Config.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Config.java new file mode 100644 index 0000000..c38b945 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Config.java @@ -0,0 +1,30 @@ +package com.kusitms29.backendH.infra.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsS3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + @Bean + public AmazonS3Client amazonS3Client() { + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey))) + .build(); + } + + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Service.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Service.java new file mode 100644 index 0000000..b28d7b6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/AwsS3Service.java @@ -0,0 +1,92 @@ +package com.kusitms29.backendH.infra.config; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.kusitms29.backendH.global.error.exception.InternalServerException; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class AwsS3Service { + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + + public String uploadImage(MultipartFile image) { + String fileName = createFileName(image.getOriginalFilename()); + String fileUrl = amazonS3.getUrl(bucket, fileName).toString(); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(image.getSize()); + objectMetadata.setContentType(image.getContentType()); + try { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, image.getInputStream(), objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch(IOException e) { + throw new InternalServerException(S3_UPLOAD_ERROR); + } + return fileUrl; + } + + public List uploadImages(List images) { + List fileNameList = new ArrayList<>(); + List fileUrlList = new ArrayList<>(); + + images.forEach(file -> { + String fileName = createFileName(file.getOriginalFilename()); + String fileUrl = amazonS3.getUrl(bucket, fileName).toString(); + + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(String.valueOf(file.getSize())); + objectMetadata.setContentType(file.getContentType()); + + try(InputStream inputStream = file.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch(IOException e) { + log.error(e.getMessage()); + throw new InternalServerException(S3_UPLOAD_ERROR); + } + fileNameList.add(fileName); + fileUrlList.add(fileUrl); + }); + + return fileUrlList; + } + public void deleteImage(String fileName) { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } + public String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + try { + return fileName.substring(fileName.lastIndexOf(".")); + } catch(StringIndexOutOfBoundsException e) { + throw new InvalidValueException(INVALID_IMAGE_TYPE); + } + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/CorsConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/CorsConfig.java new file mode 100644 index 0000000..774d0c6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/CorsConfig.java @@ -0,0 +1,25 @@ +package com.kusitms29.backendH.infra.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; + +@Configuration +public class CorsConfig { + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("http://localhost:3000"); + config.addAllowedOrigin(""); + config.addAllowedHeader("*"); + config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + source.registerCorsConfiguration("/**",config); + return new CorsFilter(source); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/EmailConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/EmailConfig.java new file mode 100644 index 0000000..dd0f167 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/EmailConfig.java @@ -0,0 +1,68 @@ +package com.kusitms29.backendH.infra.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class EmailConfig { + @Value("${mail.host}") + private String host; + + @Value("${mail.port}") + private int port; + + @Value("${mail.username}") + private String username; + + @Value("${mail.password}") + private String password; + + @Value("${mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${mail.properties.mail.smtp.starttls.enable}") + private boolean starttlsEnable; + + @Value("${mail.properties.mail.smtp.starttls.required}") + private boolean starttlsRequired; + + @Value("${mail.properties.mail.smtp.connectiontimeout}") + private int connectionTimeout; + + @Value("${mail.properties.mail.smtp.timeout}") + private int timeout; + + @Value("${mail.properties.mail.smtp.writetimeout}") + private int writeTimeout; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + mailSender.setDefaultEncoding("UTF-8"); + mailSender.setJavaMailProperties(getMailProperties()); + + return mailSender; + } + + private Properties getMailProperties() { + Properties properties = new Properties(); + properties.put("mail.smtp.auth", auth); + properties.put("mail.smtp.starttls.enable", starttlsEnable); + properties.put("mail.smtp.starttls.required", starttlsRequired); + properties.put("mail.smtp.connectiontimeout", connectionTimeout); + properties.put("mail.smtp.timeout", timeout); + properties.put("mail.smtp.writetimeout", writeTimeout); + + return properties; + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/FCMConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/FCMConfig.java new file mode 100644 index 0000000..f42d19d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/FCMConfig.java @@ -0,0 +1,38 @@ +package com.kusitms29.backendH.infra.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +@Configuration +public class FCMConfig { + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("firebase/AccountKey.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/Main/src/main/java/com/kusitms29/backendH/infra/config/JpaConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/JpaConfig.java new file mode 100644 index 0000000..d99614c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/JpaConfig.java @@ -0,0 +1,10 @@ +package com.kusitms29.backendH.infra.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaConfig { +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/RedisConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/RedisConfig.java new file mode 100644 index 0000000..ec5a796 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/RedisConfig.java @@ -0,0 +1,48 @@ +package com.kusitms29.backendH.infra.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@RequiredArgsConstructor +@Configuration +@EnableRedisRepositories +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { //lettuce + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(port); + redisStandaloneConfiguration.setPassword(password); + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().build(); + + return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig); + } + + @Bean + public RedisTemplate redisTemplate() { //redis-cli 사용을 위한 설정 + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/RedisService.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/RedisService.java new file mode 100644 index 0000000..a25c213 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/RedisService.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.infra.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.concurrent.TimeUnit; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + public void setValues(String key, String value) { + redisTemplate.opsForValue().set(key, value); + } + @Transactional + public void setValuesWithTimeout(String key, String value, long timeout) { // 만료 시간을 설정해서 자동 삭제 가능 + redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MILLISECONDS); + } + public String getValues(String key) { + return redisTemplate.opsForValue().get(key); + } + public void deleteValues(String key) { + redisTemplate.delete(key); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/RestTemplateConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/RestTemplateConfig.java new file mode 100644 index 0000000..97a61ab --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/RestTemplateConfig.java @@ -0,0 +1,22 @@ +package com.kusitms29.backendH.infra.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.Charset; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder + .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())) + .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8"))) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/ScheduleConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/ScheduleConfig.java new file mode 100644 index 0000000..b71ef3f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/ScheduleConfig.java @@ -0,0 +1,9 @@ +package com.kusitms29.backendH.infra.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class ScheduleConfig { +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/SecurityConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/SecurityConfig.java new file mode 100644 index 0000000..afbd745 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/SecurityConfig.java @@ -0,0 +1,52 @@ +package com.kusitms29.backendH.infra.config; + + +import com.kusitms29.backendH.infra.config.auth.ExceptionHandlerFilter; +import com.kusitms29.backendH.infra.config.auth.JwtAuthenticationEntryPoint; +import com.kusitms29.backendH.infra.config.auth.JwtAuthenticationFilter; +import com.kusitms29.backendH.infra.config.auth.JwtProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@RequiredArgsConstructor +@EnableWebSecurity +@Configuration +public class SecurityConfig { + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final CorsConfig corsConfig; + private final JwtProvider jwtProvider; + // TODO api 추가될 때 white list url 확인해서 추가하기. + + private static final String[] whiteList = {"/api/auth/signin","/**"}; + + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().requestMatchers(whiteList);} + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagementConfigurer -> + sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exceptionHandlingConfigurer -> + exceptionHandlingConfigurer.authenticationEntryPoint(jwtAuthenticationEntryPoint)) + .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> + authorizationManagerRequestMatcherRegistry.anyRequest().authenticated()) + .addFilter(corsConfig.corsFilter()) + .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new ExceptionHandlerFilter(), JwtAuthenticationFilter.class) + .build(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/WebConfig.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/WebConfig.java new file mode 100644 index 0000000..9390bb9 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/WebConfig.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.infra.config; + +import com.kusitms29.backendH.infra.config.auth.UserIdArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final UserIdArgumentResolver userIdArgumentResolver; + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true); + } + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdArgumentResolver); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/ExceptionHandlerFilter.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/ExceptionHandlerFilter.java new file mode 100644 index 0000000..d19825a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/ExceptionHandlerFilter.java @@ -0,0 +1,54 @@ +package com.kusitms29.backendH.infra.config.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kusitms29.backendH.global.error.dto.ErrorBaseResponse; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import com.kusitms29.backendH.global.error.exception.UnauthorizedException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.kusitms29.backendH.global.error.ErrorCode.INTERNAL_SERVER_ERROR; + + +@Slf4j +public class ExceptionHandlerFilter extends OncePerRequestFilter { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (UnauthorizedException e) { + handleUnauthorizedException(response, e); + } catch (Exception ee) { + handleException(response); + } + } + + private void handleUnauthorizedException(HttpServletResponse response, Exception e) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + if (e instanceof UnauthorizedException ue) { + response.setStatus(ue.getErrorCode().getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(ue.getErrorCode()))); + } else if (e instanceof InvalidValueException ie) { + response.setStatus(ie.getErrorCode().getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(ie.getErrorCode()))); + } + } + + private void handleException(HttpServletResponse response) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + response.setStatus(INTERNAL_SERVER_ERROR.getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(INTERNAL_SERVER_ERROR))); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationEntryPoint.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..8a4f662 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationEntryPoint.java @@ -0,0 +1,30 @@ +package com.kusitms29.backendH.infra.config.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kusitms29.backendH.global.error.ErrorCode; +import com.kusitms29.backendH.global.error.dto.ErrorBaseResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + handleException(response); + } + + private void handleException(HttpServletResponse response) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + response.setStatus(ErrorCode.UNAUTHORIZED.getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(ErrorCode.UNAUTHORIZED))); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationFilter.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..04db41c --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtAuthenticationFilter.java @@ -0,0 +1,48 @@ +package com.kusitms29.backendH.infra.config.auth; + + +import com.kusitms29.backendH.global.error.exception.UnauthorizedException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_ACCESS_TOKEN; + + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + final String accessToken = getAccessTokenFromHttpServletRequest(request); + jwtProvider.validateAccessToken(accessToken); + final Long userId = jwtProvider.getSubject(accessToken); + setAuthentication(request, userId); + filterChain.doFilter(request, response); + } + + private String getAccessTokenFromHttpServletRequest(HttpServletRequest request) { + String accessToken = request.getHeader(AUTHORIZATION); + if (StringUtils.hasText(accessToken) && accessToken.startsWith(BEARER)) { + return accessToken.substring(BEARER.length()); + } + throw new UnauthorizedException(INVALID_ACCESS_TOKEN); + } + + private void setAuthentication(HttpServletRequest request, Long userId) { + UserAuthentication authentication = new UserAuthentication(userId, null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtProvider.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtProvider.java new file mode 100644 index 0000000..ec11627 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/JwtProvider.java @@ -0,0 +1,86 @@ +package com.kusitms29.backendH.infra.config.auth; + + +import com.kusitms29.backendH.global.error.exception.UnauthorizedException; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + + +@Getter +@Component +public class JwtProvider { + @Value("${jwt.secret}") + private String secretKey; + @Value("${jwt.access-token-expire-time}") + private long ACCESS_TOKEN_EXPIRE_TIME; + @Value("${jwt.refresh-token-expire-time}") + private long REFRESH_TOKEN_EXPIRE_TIME; + + public TokenInfo issueToken(Long userId) { + return TokenInfo.of(generateToken(userId, true), generateToken(userId, false)); + } + + public void validateAccessToken(String accessToken) { + try { + getJwtParser().parseClaimsJws(accessToken); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(EXPIRED_ACCESS_TOKEN); + } catch (Exception e) { + throw new UnauthorizedException(INVALID_ACCESS_TOKEN_VALUE); + } + } + + public void validateRefreshToken(String refreshToken) { + try { + getJwtParser().parseClaimsJws(refreshToken); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(EXPIRED_REFRESH_TOKEN); + } catch (Exception e) { + throw new UnauthorizedException(INVALID_REFRESH_TOKEN_VALUE); + } + } + + public void equalsRefreshToken(String providedRefreshToken, String storedRefreshToken) { + if (!providedRefreshToken.equals(storedRefreshToken)) { + throw new UnauthorizedException(NOT_MATCH_REFRESH_TOKEN); + } + } + + public Long getSubject(String token) { + return Long.valueOf(getJwtParser().parseClaimsJws(token) + .getBody() + .getSubject()); + } + + private String generateToken(Long userId, boolean isAccessToken) { + final Date now = new Date(); + final Date expiration = new Date(now.getTime() + (isAccessToken ? ACCESS_TOKEN_EXPIRE_TIME : REFRESH_TOKEN_EXPIRE_TIME)); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setSubject(String.valueOf(userId)) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private JwtParser getJwtParser() { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build(); + } + + private Key getSigningKey() { + String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); + return Keys.hmacShaKeyFor(encoded.getBytes()); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/TokenInfo.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/TokenInfo.java new file mode 100644 index 0000000..d10c91a --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/TokenInfo.java @@ -0,0 +1,17 @@ +package com.kusitms29.backendH.infra.config.auth; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class TokenInfo { + private String accessToken; + private String refreshToken; + + public static TokenInfo of(String accessToken, String refreshToken) { + return new TokenInfo(accessToken, refreshToken); + } +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserAuthentication.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserAuthentication.java new file mode 100644 index 0000000..27e0326 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserAuthentication.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.infra.config.auth; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserId.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserId.java new file mode 100644 index 0000000..34e0dc4 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserId.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.infra.config.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserId { +} + diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserIdArgumentResolver.java b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserIdArgumentResolver.java new file mode 100644 index 0000000..42d54d3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/config/auth/UserIdArgumentResolver.java @@ -0,0 +1,26 @@ +package com.kusitms29.backendH.infra.config.auth; + +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class UserIdArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasUserIdAnnotation = parameter.hasParameterAnnotation(UserId.class); + boolean hasLongType = Long.class.isAssignableFrom(parameter.getParameterType()); + return hasUserIdAnnotation && hasLongType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/CountryDataClient.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/CountryDataClient.java new file mode 100644 index 0000000..d3ee354 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/CountryDataClient.java @@ -0,0 +1,60 @@ +package com.kusitms29.backendH.infra.external; + +import com.kusitms29.backendH.api.user.service.dto.response.CountryResponseDto; +import com.kusitms29.backendH.domain.sync.entity.Language; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class CountryDataClient { + @Value("${openData.url}") + private String apiUrl; + @Value("${openData.authorization}") + private String authorization; + + private final RestTemplate restTemplate; + public List listOfCountries(Integer page, Integer perPage, String language) { + Language lan = Language.getEnumLanguageFromStringLanguage(language); + CountryResponseDto countryResponseDto = calloutCountryAPI(page, perPage); + return countryResponseDto.getData().stream() + .map(data -> lan.getStringLanguage().equals("korean") ? data.get한글명() : data.get영문명()) + .toList(); + } + private CountryResponseDto calloutCountryAPI(Integer page, Integer perPage) { + StringBuilder authSB = new StringBuilder(); + authSB.append("Infuser "); authSB.append(authorization); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", authSB.toString()); + + HttpEntity> requestEntity = new HttpEntity<>(headers); + + URI apiUrlWithQuery = UriComponentsBuilder.fromHttpUrl(apiUrl) + .queryParam("page", page) + .queryParam("perPage", perPage) + .build() + .toUri(); + + ResponseEntity countryResponse = restTemplate.exchange( + apiUrlWithQuery, HttpMethod.GET, requestEntity, CountryResponseDto.class + ); + return countryResponse.getBody(); + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/SchoolEmailClient.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/SchoolEmailClient.java new file mode 100644 index 0000000..032d0ad --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/SchoolEmailClient.java @@ -0,0 +1,99 @@ +package com.kusitms29.backendH.infra.external; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kusitms29.backendH.api.user.service.dto.request.schoolEmail.CalloutSchoolEmailRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.schoolEmail.CalloutSchoolEmailVerificationRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.schoolEmail.SchoolEmailRequestDto; +import com.kusitms29.backendH.api.user.service.dto.request.schoolEmail.SchoolEmailVerificationRequestDto; +import com.kusitms29.backendH.api.user.service.dto.response.schoolEmail.CalloutErrorResponse; +import com.kusitms29.backendH.api.user.service.dto.response.schoolEmail.CalloutSchoolEmailVerificationResponseDto; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +import static com.kusitms29.backendH.global.error.ErrorCode.*; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class SchoolEmailClient { + @Value("${unvicert.endpoint}") + private String apiUrl; + + @Value("${unvicert.key}") + private String key; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + public CalloutErrorResponse callOutSendSchoolEmail(SchoolEmailRequestDto requestDto) { + CalloutSchoolEmailRequestDto calloutSchoolEmailRequestDto = new CalloutSchoolEmailRequestDto( + key, requestDto.getEmail(), requestDto.getUnivName(), true + ); + HttpEntity entity = new HttpEntity<>(calloutSchoolEmailRequestDto); + ResponseEntity response; + try { + response = restTemplate.exchange( + apiUrl + "/certify", HttpMethod.POST, entity, CalloutErrorResponse.class + ); + return response.getBody(); + } catch(HttpClientErrorException.BadRequest ex) { //400 에러에 대한 에러 메세지 다양 + String message = ""; + try { + JsonNode jsonNode = objectMapper.readTree(ex.getResponseBodyAsString()); + message = jsonNode.has("message") ? jsonNode.get("message").asText() : "Unknown error"; + } catch (Exception e) { + log.info("Error parsing JSON: " + e.getMessage()); + } + if(message.equals("이미 완료된 요청입니다.")) { + throw new InvalidValueException(DUPLICATE_SCHOOL_MAIL); + } else if(message.equals("대학과 일치하지 않는 메일 도메인입니다.")) { + throw new InvalidValueException(INVALID_UNIVERSITY_DOMAIN); + } else { + throw new InvalidValueException(UNIVERSITY_API_FAIL_ERROR); + } + } + } + public CalloutSchoolEmailVerificationResponseDto callOutAuthSchoolEmail(SchoolEmailVerificationRequestDto requestDto) { + CalloutSchoolEmailVerificationRequestDto calloutSchoolEmailVerificationRequestDto = new CalloutSchoolEmailVerificationRequestDto( + key, requestDto.getEmail(), requestDto.getUnivName(), requestDto.getCode() + ); + HttpEntity entity = new HttpEntity<>(calloutSchoolEmailVerificationRequestDto); + ResponseEntity response = restTemplate.exchange( + apiUrl + "/certifycode", HttpMethod.POST, entity, CalloutSchoolEmailVerificationResponseDto.class + ); + if(!response.getBody().isSuccess()) { + throw new InvalidValueException(INVALID_AUTH_CODE); + } + return response.getBody(); + } + + public CalloutErrorResponse clearAuthCode() { + Map requestBody = new HashMap<>(); + requestBody.put("key", key); + HttpEntity> entity = new HttpEntity<>(requestBody); + + ResponseEntity response; + try { + response = restTemplate.exchange( + apiUrl + "/clear", HttpMethod.POST, entity, CalloutErrorResponse.class + ); + return response.getBody(); + } catch (HttpClientErrorException.BadRequest ex) { + throw new InvalidValueException(UNIVERSITY_API_FAIL_ERROR); + } + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/SeoulAddressClient.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/SeoulAddressClient.java new file mode 100644 index 0000000..6b42bcb --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/SeoulAddressClient.java @@ -0,0 +1,47 @@ +package com.kusitms29.backendH.infra.external; + +import com.kusitms29.backendH.api.sync.service.dto.response.SeoulAddressResponse; +import com.kusitms29.backendH.global.error.exception.InternalServerException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +import static com.kusitms29.backendH.global.error.ErrorCode.SEOUL_ADDRESS_FAIL_ERROR; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class SeoulAddressClient { + @Value("${address.endpoint}") + private String apiUrl; + + private final RestTemplate restTemplate; + + public SeoulAddressResponse calloutSeoulAddressAPI() { + + URI apiUrlWithQuery = UriComponentsBuilder.fromHttpUrl(apiUrl) + .queryParam("regcode_pattern", "11*00000") + .queryParam("is_ignore_zero", true) + .build() + .toUri(); + + try { + ResponseEntity seoulAddressResponse = restTemplate.exchange( + apiUrlWithQuery, HttpMethod.GET, null, SeoulAddressResponse.class + ); + return seoulAddressResponse.getBody(); + } catch (RestClientException e) { + throw new InternalServerException(SEOUL_ADDRESS_FAIL_ERROR); + } + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/UniversityClient.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/UniversityClient.java new file mode 100644 index 0000000..67cd142 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/UniversityClient.java @@ -0,0 +1,45 @@ +package com.kusitms29.backendH.infra.external; + +import com.kusitms29.backendH.api.user.service.dto.response.schoolEmail.CalloutErrorResponse; +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +import static com.kusitms29.backendH.global.error.ErrorCode.INVALID_UNIVERSITY_NAME; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class UniversityClient { + @Value("${unvicert.endpoint}") + private String apiUrl; + + @Value("${unvicert.key}") + private String key; + + private final RestTemplate restTemplate; + + public void isValidUniversity(String university) { + Map requestBody = new HashMap<>(); + requestBody.put("univName", university); + HttpEntity> entity = new HttpEntity<>(requestBody); + ResponseEntity response = restTemplate.exchange( + apiUrl + "/check", HttpMethod.POST, entity, CalloutErrorResponse.class + ); + if(!response.getBody().isSuccess()) { + throw new InvalidValueException(INVALID_UNIVERSITY_NAME); + }; + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocation.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocation.java new file mode 100644 index 0000000..3214e17 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocation.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.infra.external.clova.map; + +import lombok.Getter; + +@Getter +public class GeoLocation { + private String country; + private String code; + private String r1; + private String r2; + private String r3; + private double lat; +// @JsonProperty("long") +// private double longitude; + private String net; + + // Getters and Setters +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationResponse.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationResponse.java new file mode 100644 index 0000000..27ef7f6 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationResponse.java @@ -0,0 +1,10 @@ +package com.kusitms29.backendH.infra.external.clova.map; + +import lombok.Getter; + +@Getter +public class GeoLocationResponse{ + private int returnCode; + private String requestId; + private GeoLocation geoLocation; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationService.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationService.java new file mode 100644 index 0000000..10cc738 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/map/GeoLocationService.java @@ -0,0 +1,120 @@ +package com.kusitms29.backendH.infra.external.clova.map; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +@Service +@RequiredArgsConstructor +public class GeoLocationService { + private final RestTemplate restTemplate; + @Value("${api.cloud.geolocation.endpoint}") + private String endpoint; + @Value("${api.cloud.geolocation.access-key}") + private String accessKey; + @Value("${api.cloud.geolocation.secret-key}") + private String secretKey; + +// public GeoLocationService(RestTemplateBuilder restTemplateBuilder, +// @Value("${naver.cloud.geolocation.endpoint}") String endpoint, +// @Value("${naver.cloud.geolocation.access-key}") String accessKey, +// @Value("${naver.cloud.geolocation.secret-key}") String secretKey) { +// this.restTemplate = restTemplateBuilder.build(); +// this.endpoint = endpoint; +// this.accessKey = accessKey; +// this.secretKey = secretKey; +// } +public GeoLocation getGeoLocation(String ip) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException { + String url = endpoint + "/geolocation/v2/geoLocation"; + String timestamp = String.valueOf(System.currentTimeMillis()); + String signature = generateSignature(timestamp); + + HttpHeaders headers = new HttpHeaders(); + headers.set("x-ncp-apigw-timestamp", timestamp); + headers.set("x-ncp-iam-access-key", accessKey); + headers.set("x-ncp-apigw-signature-v2", signature); + + // 요청 파라미터 설정 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("ip", "219.255.158.170"); +// params.add("ext", "t"); +// params.add("responseFormatType", "json"); + + // 헤더와 파라미터를 포함한 HttpEntity 생성 + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + requestEntity, + GeoLocationResponse.class); + + return response.getBody().getGeoLocation(); +} +// public GeoLocation getGeoLocation(String ip) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException { +// String url = "https://geolocation.apigw.ntruss.com/geolocation/v2/geoLocation"; +// String timestamp = String.valueOf(System.currentTimeMillis()); +// String signature = generateSignature(timestamp); +// +// HttpHeaders headers = new HttpHeaders(); +// headers.set("x-ncp-apigw-timestamp", timestamp); +// headers.set("x-ncp-iam-access-key", accessKey); +// headers.set("x-ncp-apigw-signature-v2", signature); +// +// // 요청 파라미터 설정 +// UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url) +// .queryParam("ip", "219.255.158.170") +// .queryParam("ext", "t") +// .queryParam("responseFormatType", "json"); +// +// HttpEntity requestEntity = new HttpEntity<>(headers); +// +// ResponseEntity response = restTemplate.exchange( +// builder.toUriString(), +// HttpMethod.GET, +// requestEntity, +// GeoLocationResponse.class); +// +// return response.getBody().getGeoLocation(); +// } + + private String generateSignature(String timestamp) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException, UnsupportedEncodingException { + String space = " "; + String newLine = "\n"; + String method = "GET"; + String url = "/geolocation/v2/geoLocation"; + + String message = new StringBuilder() + .append(method) + .append(space) + .append(url) + .append(newLine) + .append(timestamp) + .append(newLine) + .append(accessKey) + .toString(); + + SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(signingKey); + + byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8")); + String encodeBase64String = Base64.getEncoder().encodeToString(rawHmac); + + return encodeBase64String; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/PapagoService.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/PapagoService.java new file mode 100644 index 0000000..a12f94d --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/PapagoService.java @@ -0,0 +1,99 @@ +package com.kusitms29.backendH.infra.external.clova.papago; + +import com.kusitms29.backendH.global.error.exception.InvalidValueException; +import com.kusitms29.backendH.infra.external.clova.papago.detection.LanguageDetectionRequest; +import com.kusitms29.backendH.infra.external.clova.papago.detection.LanguageDetectionResponse; +import com.kusitms29.backendH.infra.external.clova.papago.translation.TextTranslationRequest; +import com.kusitms29.backendH.infra.external.clova.papago.translation.TextTranslationResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +import static com.kusitms29.backendH.global.error.ErrorCode.PAPAGO_FAIL_ERROR; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PapagoService { + + private final RestTemplate restTemplate; + @Value("${api.cloud.textTranslation.endpoint}") + private String endpoint; + @Value("${api.cloud.textTranslation.access-key}") + private String accessKey; + @Value("${api.cloud.textTranslation.secret-key}") + private String secretKey; + + public TextTranslationResponse translateText(TextTranslationRequest requestDto) { + String url = endpoint + "/nmt/v1/translation"; + HttpHeaders headers = new HttpHeaders(); + headers.set("X-NCP-APIGW-API-KEY-ID", accessKey); + headers.set("X-NCP-APIGW-API-KEY", secretKey); + + Map requestBody = new HashMap<>(); + requestBody.put("source", requestDto.getSource()); + requestBody.put("target", requestDto.getTarget()); + requestBody.put("text", requestDto.getText()); + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + + ResponseEntity response; + try { + response = restTemplate.exchange( + url, HttpMethod.POST, entity, TextTranslationResponse.class + ); + + log.info("Response status code: {}", response.getStatusCode()); + log.info("Response headers: {}", response.getHeaders()); + log.info("Response body: {}", response.getBody()); + if(response.getBody() != null) { + return response.getBody(); + } + return response.getBody(); + } catch (HttpClientErrorException e) { + log.info("e.getMessage() :: " + e.getMessage()); + throw new InvalidValueException(PAPAGO_FAIL_ERROR); + } + + } + public LanguageDetectionResponse checkLanguage(LanguageDetectionRequest requestDto) { + String url = endpoint + "/langs/v1/dect"; + HttpHeaders headers = new HttpHeaders(); + headers.set("X-NCP-APIGW-API-KEY-ID", accessKey); + headers.set("X-NCP-APIGW-API-KEY", secretKey); + + Map requestBody = new HashMap<>(); + requestBody.put("query", requestDto.getQuery()); + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + + ResponseEntity response; + try { + response = restTemplate.exchange( + url, HttpMethod.POST, entity, LanguageDetectionResponse.class + ); + + log.info("Response status code: {}", response.getStatusCode()); + log.info("Response headers: {}", response.getHeaders()); + log.info("Response body: {}", response.getBody()); + if(response.getBody() != null) { + return response.getBody(); + } + return response.getBody(); + } catch (HttpClientErrorException e) { + log.info("e.getMessage() :: " + e.getMessage()); + throw new InvalidValueException(PAPAGO_FAIL_ERROR); + } + } + +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionRequest.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionRequest.java new file mode 100644 index 0000000..1e50231 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionRequest.java @@ -0,0 +1,8 @@ +package com.kusitms29.backendH.infra.external.clova.papago.detection; + +import lombok.Getter; + +@Getter +public class LanguageDetectionRequest { + String query; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionResponse.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionResponse.java new file mode 100644 index 0000000..6d9f15f --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/detection/LanguageDetectionResponse.java @@ -0,0 +1,8 @@ +package com.kusitms29.backendH.infra.external.clova.papago.detection; + +import lombok.Getter; + +@Getter +public class LanguageDetectionResponse { + private String langCode; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationRequest.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationRequest.java new file mode 100644 index 0000000..c6d4d9b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationRequest.java @@ -0,0 +1,12 @@ +package com.kusitms29.backendH.infra.external.clova.papago.translation; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TextTranslationRequest { + private String source; //원본 언어 코드 + private String target; //변역 결과 언어 코드 + private String text; //번역할 텍스트 +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationResponse.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationResponse.java new file mode 100644 index 0000000..f017b18 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/clova/papago/translation/TextTranslationResponse.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.infra.external.clova.papago.translation; + +import lombok.Getter; + +@Getter +public class TextTranslationResponse { + public TextTranslationMessage message; + + @Getter + public static class TextTranslationMessage { + private TextTranslationResult result; + + @Getter + public static class TextTranslationResult { + private String srcLangType; + private String tarLangType; + private String translatedText; + } + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/FCMScheduler.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/FCMScheduler.java new file mode 100644 index 0000000..4b319ac --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/FCMScheduler.java @@ -0,0 +1,21 @@ +package com.kusitms29.backendH.infra.external.fcm; + +import com.kusitms29.backendH.infra.external.fcm.service.PushNotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Scheduled; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class FCMScheduler { + private final PushNotificationService pushNotificationService; + //@Scheduled(cron = "0 00 09 * * *") //오전 9시 + //@Scheduled(initialDelay = 0, fixedDelay = 7000) + public void sendSyncReminder() { + log.info("=== SYNREMINDER START ==="); + pushNotificationService.sendSyncReminder(); + log.info("=== SYNREMINDER END ==="); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/MessageTemplate.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/MessageTemplate.java new file mode 100644 index 0000000..3f8166b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/MessageTemplate.java @@ -0,0 +1,20 @@ +package com.kusitms29.backendH.infra.external.fcm; + +import lombok.Getter; + +@Getter +public enum MessageTemplate { + COMMENT("커뮤니티","\"%s\"글에 \"%s\"님이 댓글을 달았어요."), + CHAT("채팅","\"%s\"의 새로운 메세지가 도착했어요."), + CHAT_ROOM_NOTICE("공지", "\"%s\" 싱크의 새로운 채팅방이 생겼어요."), + SYNC_REMINDER("일정", "%s님! 오늘은 \"%s\" 싱크하는 날이에요."), + REVIEW("후기","%s님! 즐거운 싱크 되셨나요?"); + + private final String title; + private final String content; + + MessageTemplate(String title, String content) { + this.title = title; + this.content = content; + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/repository/FCMTokenRepository.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/repository/FCMTokenRepository.java new file mode 100644 index 0000000..c788f2b --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/repository/FCMTokenRepository.java @@ -0,0 +1,28 @@ +package com.kusitms29.backendH.infra.external.fcm.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FCMTokenRepository { + private final StringRedisTemplate tokenRedisTemplate; + + public void saveToken(String userId, String fcmToken) { + tokenRedisTemplate.opsForValue() + .set(userId, fcmToken); + } + + public String getToken(String userId) { + return tokenRedisTemplate.opsForValue().get(userId); + } + + public void deleteToken(String userId) { + tokenRedisTemplate.delete(userId); + } + + public boolean hasKey(String userId) { + return tokenRedisTemplate.hasKey(userId); + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/PushNotificationService.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/PushNotificationService.java new file mode 100644 index 0000000..9c78c10 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/PushNotificationService.java @@ -0,0 +1,235 @@ +package com.kusitms29.backendH.infra.external.fcm.service; + +import com.google.firebase.messaging.*; +import com.kusitms29.backendH.domain.comment.entity.Comment; +import com.kusitms29.backendH.domain.notification.entity.NotificationHistory; +import com.kusitms29.backendH.domain.notification.entity.NotificationType; +import com.kusitms29.backendH.domain.notification.entity.TopCategory; +import com.kusitms29.backendH.domain.notification.repository.NotificationHistoryRepository; +import com.kusitms29.backendH.domain.post.entity.Post; +import com.kusitms29.backendH.domain.post.service.PostReader; +import com.kusitms29.backendH.domain.sync.entity.Sync; +import com.kusitms29.backendH.domain.sync.service.SyncReader; +import com.kusitms29.backendH.domain.user.entity.User; +import com.kusitms29.backendH.domain.user.service.UserReader; +import com.kusitms29.backendH.global.common.TimeCalculator; +import com.kusitms29.backendH.infra.external.fcm.MessageTemplate; +import com.kusitms29.backendH.domain.sync.repository.SyncRepository; +import com.kusitms29.backendH.infra.external.fcm.repository.FCMTokenRepository; +import com.kusitms29.backendH.infra.external.fcm.service.dto.NotificationDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PushNotificationService { + private final FirebaseMessaging firebaseMessaging; + private final FCMTokenRepository fcmTokenRepository; + private final SyncRepository syncRepository; + private final NotificationHistoryRepository notificationHistoryRepository; + private final UserReader userReader; + private final SyncReader syncReader; + private final PostReader postReader; + + @Transactional + public void sendSyncReminder() { + //오늘 자정 + LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0); + List> hurrySyncInfo = syncRepository.findHurrySyncInfo(today); + log.info("hurrySyncInfo.size() :: " + hurrySyncInfo.size()); + for (Map userInfo : hurrySyncInfo) { + + NotificationDto dto = NotificationDto.getSyncReminderAlarm( + (Long) userInfo.get("user_id"), + (String) userInfo.get("user_name"), + (String) userInfo.get("sync_name"), + MessageTemplate.SYNC_REMINDER, + (Long) userInfo.get("sync_id") + ); + sendMessage(dto); + + //sync_type : 지속성일 때, 모임날짜가 지났다면 다음 요일 날짜로 업데이트 + LocalDateTime now = LocalDateTime.now(); + LocalDateTime current_routine_date = LocalDateTime.parse(userInfo.get("effective_date").toString(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.S")); + + if(userInfo.get("sync_type").toString().equals("LONGTIME") && current_routine_date.isBefore(now)) { + String regularDay = userInfo.get("regular_day").toString(); + + String regularTimeString = userInfo.get("regular_time").toString(); + LocalTime regularTime = LocalTime.parse(regularTimeString, DateTimeFormatter.ofPattern("HH:mm:ss")); + + LocalDateTime nextDate = TimeCalculator.getNextWeekDate(TimeCalculator.convertStringToDayOfWeek(regularDay)); + nextDate = nextDate.with(regularTime); + + Sync sync = syncReader.findById(Long.parseLong(userInfo.get("sync_id").toString())); + sync.updateNextDate(nextDate); + } + + //알림 기록 + User alarmedUser = userReader.findByUserId(Long.parseLong(dto.getId())); + NotificationHistory history = NotificationHistory.createHistory( + alarmedUser, + dto.getTemplate().getTitle(), + createMessageBody(dto), + getToken(dto.getId()), + now, + NotificationType.SYNC_REMINDER, + TopCategory.MY_SYNC, + dto.getInfoId(), + "" + ); + notificationHistoryRepository.save(history); + } + log.info("Sync reminders sent successfully."); + } + + @Transactional + public void sendCommentNotification(Long postId, Comment newComment) { + //글 주인에게 댓글 알리기 + Post post = postReader.findById(postId); + if(post.getUser().getId() == newComment.getUser().getId()) { + return; + } + + NotificationDto dto = NotificationDto.getCommunityAlarm( + post.getUser().getId(), + post.getTitle(), + newComment.getUser().getUserName(), + MessageTemplate.COMMENT, + postId, + newComment.getId() + ); + sendMessage(dto); + + //알림 기록 + LocalDateTime now = LocalDateTime.now(); + User alarmedUser = userReader.findByUserId(Long.parseLong(dto.getId())); + NotificationHistory history = NotificationHistory.createHistory( + alarmedUser, + dto.getTemplate().getTitle(), + createMessageBody(dto), + getToken(dto.getId()), + now, + NotificationType.COMMENT, + TopCategory.ACTIVITY, + postId.toString(), + newComment.getId().toString() + ); + notificationHistoryRepository.save(history); + + log.info("comment notification sent successfully."); + } + + @Transactional + public void sendChatRoomNotice(List users, Long syncId, String roomName) { + Sync sync = syncReader.findById(syncId); + + for(User user : users) { + NotificationDto dto = NotificationDto.getChatRoomNoticeAlarm( + user.getId(), + sync.getSyncName(), + MessageTemplate.CHAT_ROOM_NOTICE, + roomName + ); + sendMessage(dto); + + //알림 기록 + LocalDateTime now = LocalDateTime.now(); + User alarmedUser = userReader.findByUserId(Long.parseLong(dto.getId())); + NotificationHistory history = NotificationHistory.createHistory( + alarmedUser, + dto.getTemplate().getTitle(), + createMessageBody(dto), + getToken(dto.getId()), + now, + NotificationType.CHAT_ROOM_NOTICE, + TopCategory.MY_SYNC, + roomName, + "" + ); + notificationHistoryRepository.save(history); + + log.info("chatRoomNotice notification sent to {} successfully. ", user.getUserName()); + } + } + + private void sendMessage(NotificationDto dto) { + //FCM 토큰 확인 + if (!hasKey(dto.getId())) { + log.warn("FCM token not found for user with ID:" + dto.getId()); + return; + } + + //메세지 보내기 + try{ + firebaseMessaging.send(createMessage(dto)); + } catch (FirebaseMessagingException e) { + if(e.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) { + log.error("FCM token for user {} is invalid or unregistered", dto.getId()); + deleteToken(dto.getId()); + } else { + log.error("Failed to send FCM message to user {}", dto.getId()); + } + } + } + + private Message createMessage(NotificationDto dto) { + AndroidConfig androidConfig = AndroidConfig.builder() + .setNotification(AndroidNotification.builder() + .setChannelId(dto.getChannelId()) + .setTitle(dto.getTemplate().getTitle()) + .setBody(createMessageBody(dto)) + .build()) + .build(); + + return Message.builder() + .setToken(getToken(dto.getId())) + .setAndroidConfig(androidConfig) + .build(); + /*return Message.builder() + .setToken(getToken(dto.getId())) + .setNotification(createNotification(dto)) + .build();*/ + } + + /*private Notification createNotification(NotificationDto dto) { + return Notification.builder() + .setTitle(dto.getTemplate().getTitle()) + .setBody(createMessageBody(dto)) + .build(); + }*/ + + private String createMessageBody(NotificationDto dto) { + if(dto.getStr2() != null && !dto.getStr2().isEmpty()) { + return String.format(dto.getTemplate().getContent(), dto.getStr1(), dto.getStr2()); + } + if(dto.getStr1() != null && !dto.getStr1().isEmpty()) { + return String.format(dto.getTemplate().getContent(), dto.getStr1()); + } + return dto.getTemplate().getContent(); + } + + public void saveToken(String id, String fcmToken) { fcmTokenRepository.saveToken(id, fcmToken); } + + public void deleteToken(String id) { + fcmTokenRepository.deleteToken(id); + } + + private String getToken(String id) { + return fcmTokenRepository.getToken(id); + } + + private boolean hasKey(String id) { + return fcmTokenRepository.hasKey(id); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/NotificationDto.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/NotificationDto.java new file mode 100644 index 0000000..d22e126 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/NotificationDto.java @@ -0,0 +1,71 @@ +package com.kusitms29.backendH.infra.external.fcm.service.dto; + +import com.kusitms29.backendH.infra.external.fcm.MessageTemplate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class NotificationDto { + private String id; //알림 받는 이 + private String str1; //알림 내역1 + private String str2; //알림 내역2 + /** + * 커뮤니티 : 글이름 + 댓글단이 -> 글 Id, 댓글 Id + * 일정 : 유저이름 + 싱크이름 -> 싱크 Id + * 채팅방 개설 공지 : 싱크이름 -> 채팅방 Id + * 채팅 : 채팅내용 -> 채팅방 Id + * + * TODO + * 후기 : 유저이름 -> 마이페이지? + */ + private MessageTemplate template; + private String infoId; + private String infoId2; + private String channelId; + + public static NotificationDto getSyncReminderAlarm(Long userId, String userName, String syncName, + MessageTemplate template, + Long syncId) { + return NotificationDto.builder() + .id(userId.toString()) + .str1(userName) + .str2(syncName) + .template(template) + .infoId(syncId.toString()) + .channelId("RemindChannel") + .build(); + } + + public static NotificationDto getCommunityAlarm(Long userId, String postName, String userName, + MessageTemplate template, + Long postId, Long commendId) { + return NotificationDto.builder() + .id(userId.toString()) + .str1(postName) + .str2(userName) + .template(template) + .infoId(postId.toString()) + .infoId2(commendId.toString()) + .channelId("CommunityChannel") + .build(); + } + + public static NotificationDto getChatRoomNoticeAlarm(Long userId, String syncName, + MessageTemplate template, + String roomName) { + + return NotificationDto.builder() + .id(userId.toString()) + .str1(syncName) + .template(template) + .infoId(roomName) + .channelId("OpenChatChannel") + .build(); + } +} \ No newline at end of file diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/SyncReminderDto.java b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/SyncReminderDto.java new file mode 100644 index 0000000..51fced7 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/external/fcm/service/dto/SyncReminderDto.java @@ -0,0 +1,18 @@ +package com.kusitms29.backendH.infra.external.fcm.service.dto; + +import com.kusitms29.backendH.infra.external.fcm.MessageTemplate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class SyncReminderDto { + private String id; + private String name; + private String syncName; + private MessageTemplate template; +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/utils/ListUtils.java b/Main/src/main/java/com/kusitms29/backendH/infra/utils/ListUtils.java new file mode 100644 index 0000000..91b5fd3 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/utils/ListUtils.java @@ -0,0 +1,16 @@ +package com.kusitms29.backendH.infra.utils; + +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ListUtils { + public List getListByTake(List dtos, int take) { + if (take == 0 || take >= dtos.size()) { + return dtos; + } else { + return dtos.subList(0, take); + } + } +} diff --git a/Main/src/main/java/com/kusitms29/backendH/infra/utils/TranslateUtil.java b/Main/src/main/java/com/kusitms29/backendH/infra/utils/TranslateUtil.java new file mode 100644 index 0000000..1cdf316 --- /dev/null +++ b/Main/src/main/java/com/kusitms29/backendH/infra/utils/TranslateUtil.java @@ -0,0 +1,61 @@ +package com.kusitms29.backendH.infra.utils; + +import com.kusitms29.backendH.infra.external.clova.papago.PapagoService; +import com.kusitms29.backendH.infra.external.clova.papago.translation.TextTranslationRequest; +import com.kusitms29.backendH.infra.external.clova.papago.translation.TextTranslationResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Field; + +@Service +@RequiredArgsConstructor +public class TranslateUtil { + private final PapagoService papagoService; + + public T translateObject(T object) { + T translatedObject = createNewInstance(object); + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + Class fieldType = field.getType(); + if (fieldType == String.class) { + field.setAccessible(true); + try { + String value = (String) field.get(object); + if (value != null) { + TextTranslationRequest requestDto = new TextTranslationRequest(); + requestDto.setSource("ko"); + requestDto.setTarget("en"); + requestDto.setText(value); + TextTranslationResponse translationResponse = papagoService.translateText(requestDto); + field.set(translatedObject, translationResponse.getMessage().getResult().getTranslatedText()); + } else { + field.set(translatedObject, null); + } + } catch (IllegalAccessException e) { + // log.error("Error while translating object", e); + } + } else { + field.setAccessible(true); + try { + Object value = field.get(object); + field.set(translatedObject, value); + } catch (IllegalAccessException e) { + // log.error("Error while setting non-string field", e); + } + } + } + return translatedObject; + } + + @SuppressWarnings("unchecked") + private T createNewInstance(T object) { + try { + Class clazz = (Class) object.getClass(); + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + // log.error("Error while creating new instance", e); + throw new RuntimeException("Failed to create new instance", e); + } + } +} \ No newline at end of file diff --git a/Main/src/test/java/com/kusitms29/backendH/BackendHApplicationTests.java b/Main/src/test/java/com/kusitms29/backendH/BackendHApplicationTests.java new file mode 100644 index 0000000..149515d --- /dev/null +++ b/Main/src/test/java/com/kusitms29/backendH/BackendHApplicationTests.java @@ -0,0 +1,13 @@ +package com.kusitms29.backendH; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BackendHApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..5420ba5 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +IS_GREEN_EXIST=$(docker ps | grep green) +DEFAULT_CONF=" /etc/nginx/nginx.conf" + +# blue가 실행 중이면 green을 up합니다. +if [ -z $IS_GREEN_EXIST ];then + docker-compose down + echo "### BLUE => GREEN ####" + echo ">>> green image를 pull합니다." + docker-compose pull green + echo ">>> green container를 up합니다." + docker-compose up -d --remove-orphans green + while [ 1 = 1 ]; do + echo ">>> green health check 중..." + sleep 3 + REQUEST=$(curl http://127.0.0.1:8082) + if [ -n "$REQUEST" ]; then + echo ">>> 🍃 health check success !" + break; + fi + done; + sleep 3 + echo ">>> nginx를 다시 실행 합니다." + sudo cp /etc/nginx/nginx.green.conf /etc/nginx/nginx.conf + sudo nginx -s reload + echo ">>> blue container를 down합니다." + docker-compose stop blue + +# green이 실행 중이면 blue를 up합니다. +else + docker-compose down + echo "### GREEN => BLUE ###" + echo ">>> blue image를 pull합니다." + docker-compose pull blue + echo ">>> blue container up합니다." + docker-compose up -d --remove-orphans blue + while [ 1 = 1 ]; do + echo ">>> blue health check 중..." + sleep 3 + REQUEST=$(curl http://127.0.0.1:8081) + if [ -n "$REQUEST" ]; then + echo ">>> 🍃 health check success !" + break; + fi + done; + sleep 3 + echo ">>> nginx를 다시 실행 합니다." + sudo cp /etc/nginx/nginx.blue.conf /etc/nginx/nginx.conf + sudo nginx -s reload + echo ">>> green container를 down합니다." + docker-compose stop green +fi \ No newline at end of file diff --git a/socket/.gitignore b/socket/.gitignore new file mode 100644 index 0000000..7a5803f --- /dev/null +++ b/socket/.gitignore @@ -0,0 +1,42 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Yml ### +**/src/main/resources/application.yml +### firebase ### +/src/main/resources/firebase/ \ No newline at end of file diff --git a/socket/Dockerfile b/socket/Dockerfile new file mode 100644 index 0000000..95bfd5e --- /dev/null +++ b/socket/Dockerfile @@ -0,0 +1,3 @@ +FROM openjdk:17-alpine +COPY ./build/libs/socket-0.0.1-SNAPSHOT.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/socket/build.gradle b/socket/build.gradle new file mode 100644 index 0000000..c485100 --- /dev/null +++ b/socket/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.5' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'Backend' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/socket/docker-compose.yml b/socket/docker-compose.yml new file mode 100644 index 0000000..cfbef9a --- /dev/null +++ b/socket/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' +services: + blue: + container_name: blue + image: haul123/blue-green + expose: + - 8080 + ports: + - 8081:8080 + environment: + - TZ=Asia/Seoul + green: + container_name: green + image: haul123/blue-green + expose: + - 8080 + ports: + - 8082:8080 + environment: + - TZ=Asia/Seoul \ No newline at end of file diff --git a/socket/gradle/wrapper/gradle-wrapper.jar b/socket/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/socket/gradlew.bat b/socket/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/socket/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/socket/settings.gradle b/socket/settings.gradle new file mode 100644 index 0000000..f69e1ac --- /dev/null +++ b/socket/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'socket' diff --git a/socket/src/main/java/Backend/socket/SocketApplication.java b/socket/src/main/java/Backend/socket/SocketApplication.java new file mode 100644 index 0000000..35fa5ad --- /dev/null +++ b/socket/src/main/java/Backend/socket/SocketApplication.java @@ -0,0 +1,13 @@ +package Backend.socket; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SocketApplication { + + public static void main(String[] args) { + SpringApplication.run(SocketApplication.class, args); + } + +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/ChatController.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/ChatController.java new file mode 100644 index 0000000..607fe90 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/ChatController.java @@ -0,0 +1,82 @@ +package Backend.socket.domain.chat.application.controller; + + +import Backend.socket.domain.chat.application.controller.dto.request.ChatListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageRoomRequestDto; +import Backend.socket.domain.chat.application.controller.dto.response.ChatListResponseDto; +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageListResponseDto; +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageResponseDto; +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageRoomResponseDto; +import Backend.socket.domain.chat.application.service.ChatService; +import Backend.socket.global.common.MessageSuccessCode; +import Backend.socket.global.common.MessageSuccessResponse; +import Backend.socket.global.common.image; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +@RestController +public class ChatController { + private final ChatService chatService; + private final SimpMessagingTemplate template; + private final RedisTemplate redisTemplate; + public ChatController(ChatService chatService, SimpMessagingTemplate template, + @Qualifier("chatRedisConnectionFactory") RedisConnectionFactory connectionFactory) { + this.chatService = chatService; + this.template = template; + this.redisTemplate = new RedisTemplate<>(); + this.redisTemplate.setConnectionFactory(connectionFactory); + } +// @MessageMapping("/chat/{sessionId}") +// public void sendChatMessage(@DestinationVariable("sessionId") final String sessionId, +// @RequestBody final ChatMessageRequestDto chatMessageRequestDto) { +// final ChatMessageResponseDto responseDto = chatService.createSendMessageContent(sessionId, chatMessageRequestDto); +// redisTemplate.convertAndSend("meetingRoom", responseDto); +// } + @MessageMapping("/room/{roomName}") + @SendTo("/sub/room/{roomName}") + public MessageSuccessResponse sendChatMessageInRoom(@DestinationVariable("roomName") final String roomName, + @RequestBody final ChatMessageRoomRequestDto chatMessageRoomRequestDto) throws IOException { + return MessageSuccessResponse.of(MessageSuccessCode.RECEIVED, chatService.createSendMessageContentInRoom(roomName, chatMessageRoomRequestDto).getMessage()); + } + @MessageMapping("/room/image/{roomName}") + @SendTo("/sub/room/{roomName}") + public MessageSuccessResponse sendImageMessageInRoom(@DestinationVariable("roomName") final String roomName, + @RequestBody final image image) throws IOException { + return MessageSuccessResponse.of(MessageSuccessCode.RECEIVED, chatService.createSendImageContentInRoom(roomName, image).getMessage()); + } +// @MessageMapping("/room/{roomName}") +// public void sendChatMessageInRoom(@DestinationVariable("roomName") final String roomName, +// @RequestBody final ChatMessageRoomRequestDto chatMessageRoomRequestDto) { +// final ChatMessageRoomResponseDto responseDto = chatService.createSendMessageContentInRoom(roomName, chatMessageRoomRequestDto); +// redisTemplate.convertAndSend("meetingRoom", responseDto); +// } + +// @MessageMapping("/chat/detail/{sessionId}") +// public void sendChatDetailMessage(@DestinationVariable("sessionId") final String sessionId, +// @RequestBody final ChatMessageListRequestDto chatMessageListRequestDto) { +// final ChatMessageListResponseDto responseDto = chatService.sendChatDetailMessage(sessionId, chatMessageListRequestDto); +// template.convertAndSend("/sub/chat/" + sessionId, MessageSuccessResponse.of(MessageSuccessCode.MESSAGE, responseDto)); +// } +// +// @MessageMapping("/chat/all") +// public void sendUserChatListMessage(@Header("sessionId") final String sessionId, +// @RequestBody final ChatListRequestDto chatListRequestDto) { +// final ChatListResponseDto responseDto = chatService.sendUserChatListMessage(sessionId, chatListRequestDto); +// template.convertAndSend("/sub/chat/" + sessionId, MessageSuccessResponse.of(MessageSuccessCode.CHATLIST, responseDto)); +// } + +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/RoomController.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/RoomController.java new file mode 100644 index 0000000..f7c377e --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/RoomController.java @@ -0,0 +1,57 @@ +package Backend.socket.domain.chat.application.controller; + +import Backend.socket.domain.chat.application.controller.dto.request.ChatListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageRoomRequestDto; +import Backend.socket.domain.chat.application.controller.dto.response.*; +import Backend.socket.domain.chat.application.service.ChatService; +import Backend.socket.domain.chat.application.service.RoomService; +import Backend.socket.domain.chat.domain.Chat; +import Backend.socket.global.common.MessageSuccessCode; +import Backend.socket.global.common.MessageSuccessResponse; +import Backend.socket.infra.config.auth.UserId; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessagingTemplate; +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.RestController; + +import java.io.IOException; + +@RestController +public class RoomController { + private final SimpMessagingTemplate template; + private final RedisTemplate redisTemplate; + private final RoomService roomService; + private final ChatService chatService; + public RoomController(RoomService roomService, SimpMessagingTemplate template, @Qualifier("redisTemplate") RedisTemplate redisTemplate, ChatService chatService) { + this.roomService = roomService; + this.template = template; + this.redisTemplate = redisTemplate; + this.chatService = chatService; + } + @MessageMapping("/room/detail/{roomName}") + public void sendChatDetailMessage(@DestinationVariable("roomName") final String roomName + ) { + final RoomMessageListResponseDto responseDto = roomService.sendRoomDetailMessage(roomName); + template.convertAndSend("/sub/room/" + roomName, MessageSuccessResponse.of(MessageSuccessCode.MESSAGE, responseDto)); + } + @MessageMapping("/room/all/{sessionId}") + public void sendUserChatListMessage(@DestinationVariable("sessionId") final String sessionId) { + final RoomListResponseDto responseDto = roomService.sendUserChatListMessage(sessionId); + template.convertAndSend("/sub/room/" + sessionId, MessageSuccessResponse.of(MessageSuccessCode.CHATLIST, responseDto)); + } + @PostMapping("/room/{roomName}") + public MessageSuccessResponse sendChatMessage(@PathVariable("roomName") String roomName, + @RequestBody final ChatMessageRoomRequestDto chatMessageRoomRequestDto) throws IOException { + + + return MessageSuccessResponse.of(MessageSuccessCode.RECEIVED, chatService.createSendMessageContentInRoom(roomName, chatMessageRoomRequestDto).getMessage()); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatListRequestDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatListRequestDto.java new file mode 100644 index 0000000..239df31 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatListRequestDto.java @@ -0,0 +1,11 @@ +package Backend.socket.domain.chat.application.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatListRequestDto { + private String userName; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageListRequestDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageListRequestDto.java new file mode 100644 index 0000000..d555d5c --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageListRequestDto.java @@ -0,0 +1,13 @@ +package Backend.socket.domain.chat.application.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatMessageListRequestDto { + private String chatSession; + private String fromUserName; + private String toUserName; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRequestDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRequestDto.java new file mode 100644 index 0000000..98a782e --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRequestDto.java @@ -0,0 +1,14 @@ +package Backend.socket.domain.chat.application.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatMessageRequestDto { + private String chatSession; + private String fromUserName; + private String toUserName; + private String content; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRoomRequestDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRoomRequestDto.java new file mode 100644 index 0000000..da4db3a --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/ChatMessageRoomRequestDto.java @@ -0,0 +1,18 @@ +package Backend.socket.domain.chat.application.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatMessageRoomRequestDto { + private List image; + private String chatSession; + private String fromUserName; + private String toRoomName; + private String content; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomChatMessageListReq.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomChatMessageListReq.java new file mode 100644 index 0000000..d1d5b21 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomChatMessageListReq.java @@ -0,0 +1,14 @@ +package Backend.socket.domain.chat.application.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class RoomChatMessageListReq { + private String roomSession; + private String fromUserName; + private String toRoomName; + +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomMessageListRequestDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomMessageListRequestDto.java new file mode 100644 index 0000000..f88b51b --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/request/RoomMessageListRequestDto.java @@ -0,0 +1,13 @@ +package Backend.socket.domain.chat.application.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class RoomMessageListRequestDto { + private String chatSession; + private String fromUserName; + private String toRoomName; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatListResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatListResponseDto.java new file mode 100644 index 0000000..4f690fa --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatListResponseDto.java @@ -0,0 +1,18 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class ChatListResponseDto { + private List chatList; + + public static ChatListResponseDto of(List chatList) { + return ChatListResponseDto.builder() + .chatList(chatList) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageElementResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageElementResponseDto.java new file mode 100644 index 0000000..957dd52 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageElementResponseDto.java @@ -0,0 +1,42 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import Backend.socket.domain.chat.application.service.TriFunction; +import Backend.socket.domain.chat.domain.ChatContent; +import Backend.socket.domain.chat.domain.ChatUser; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@Builder +@Getter +public class ChatMessageElementResponseDto { + private String userName; + private String content; + private String time; + private String sessionId; + private String profile; + private String images; + + +// public static List listOf(List chatContentList,String sessionId,String profile) { +// return chatContentList.stream() +// .map(chatContent -> ChatMessageElementResponseDto.of(chatContent, sessionId, profile)) +// .collect(Collectors.toList()); +// } + + + public static ChatMessageElementResponseDto of(ChatContent chatContent, String sessionId,String profile, String image) { + return ChatMessageElementResponseDto.builder() + .userName(chatContent.getUserName()) + .content(chatContent.getContent()) + .time(chatContent.getTime().toString()) + .sessionId(sessionId) + .profile(profile) + .images(image) + .build(); + } + +} + diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageListResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageListResponseDto.java new file mode 100644 index 0000000..8b5939b --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageListResponseDto.java @@ -0,0 +1,20 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class ChatMessageListResponseDto { + private ChatUserResponseDto user; + private List chatMessageList; + + public static ChatMessageListResponseDto of(ChatUserResponseDto chatUserResponseDto, List chatMessageList) { + return ChatMessageListResponseDto.builder() + .user(chatUserResponseDto) + .chatMessageList(chatMessageList) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageResponseDto.java new file mode 100644 index 0000000..f3036ea --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageResponseDto.java @@ -0,0 +1,22 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class ChatMessageResponseDto { + private String receivedUser; + private List sessionList; + private ChatMessageElementResponseDto message; + + public static ChatMessageResponseDto of(String receivedUser, List sessionList, ChatMessageElementResponseDto message) { + return ChatMessageResponseDto.builder() + .receivedUser(receivedUser) + .sessionList(sessionList) + .message(message) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageRoomResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageRoomResponseDto.java new file mode 100644 index 0000000..26ead2d --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatMessageRoomResponseDto.java @@ -0,0 +1,22 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class ChatMessageRoomResponseDto { + private String room; + private List sessionList; + private ChatMessageElementResponseDto message; + + public static ChatMessageRoomResponseDto of(String room, List sessionList, ChatMessageElementResponseDto message) { + return ChatMessageRoomResponseDto.builder() + .room(room) + .sessionList(sessionList) + .message(message) + .build(); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatUserResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatUserResponseDto.java new file mode 100644 index 0000000..b2aa51d --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/ChatUserResponseDto.java @@ -0,0 +1,25 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import Backend.socket.domain.chat.domain.ChatUser; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ChatUserResponseDto { + private String sessionId; + private String name; + private String profile; + + public static ChatUserResponseDto of(ChatUser chatUser) { + if (chatUser != null) { + return ChatUserResponseDto.builder() + .sessionId(chatUser.getSessionId()) + .name(chatUser.getName()) + .profile(chatUser.getProfile()) + .build(); + } else { + return ChatUserResponseDto.builder().build(); + } + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomChatResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomChatResponseDto.java new file mode 100644 index 0000000..299056e --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomChatResponseDto.java @@ -0,0 +1,39 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import Backend.socket.domain.chat.domain.Room; +import lombok.Builder; +import lombok.Getter; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Builder +@Getter +public class RoomChatResponseDto { + private String syncName; + private int total; + private String content; + private String time; + + + public static RoomChatResponseDto of(Room room, String content, LocalDateTime time){ + return RoomChatResponseDto.builder() + .syncName(room.getSyncName()) + .total(room.getChatUserList().size()) + .content(content) + .time(calculateTimeDifference(time)) + .build(); + + + } + public static String calculateTimeDifference(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + Duration duration = Duration.between(time, now); + long months = duration.toHours() / 60; + if (months > 0) { + return months + "분 전"; + } + return "방금 전"; + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomListResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomListResponseDto.java new file mode 100644 index 0000000..7fefb62 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomListResponseDto.java @@ -0,0 +1,19 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +@Builder +@Getter +public class RoomListResponseDto { + private String sessionId; + private List chatList; + + public static RoomListResponseDto of(String sessionId, List chatList) { + return RoomListResponseDto.builder() + .sessionId(sessionId) + .chatList(chatList) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageElementResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageElementResponseDto.java new file mode 100644 index 0000000..98a3ef6 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageElementResponseDto.java @@ -0,0 +1,44 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import Backend.socket.domain.chat.application.service.TriFunction; +import Backend.socket.domain.chat.domain.ChatContent; +import Backend.socket.domain.chat.domain.ChatUser; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@Builder +@Getter +public class RoomMessageElementResponseDto { + private ChatUserResponseDto user; + private String content; + private String time; + + public static List listOf(List chatContentList,String roomName,TriFunction formatter) { + return chatContentList.stream() + .map(chatContent -> RoomMessageElementResponseDto.of(chatContent,roomName,formatter)) + .collect(Collectors.toList()); + } + + + + public static RoomMessageElementResponseDto of(ChatContent chatContent, String roomName, TriFunction formatter) { + ChatUser chatUser = formatter.apply(roomName, chatContent.getUserName()); + if (chatUser != null) { + return RoomMessageElementResponseDto.builder() + .user(ChatUserResponseDto.of(chatUser)) + .content(chatContent.getContent()) + .time(chatContent.getTime().toString()) + .build(); + } else { + return RoomMessageElementResponseDto.builder() + .user(ChatUserResponseDto.builder().build()) + .content(chatContent.getContent()) + .time(chatContent.getTime().toString()) + .build(); + } + } + +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageListResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageListResponseDto.java new file mode 100644 index 0000000..e3c2615 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/RoomMessageListResponseDto.java @@ -0,0 +1,19 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +@Builder +@Getter +public class RoomMessageListResponseDto { + private List users; + private List chatMessageList; + + public static RoomMessageListResponseDto of(List chatUserResponseDto, List chatMessageList) { + return RoomMessageListResponseDto.builder() + .users(chatUserResponseDto) + .chatMessageList(chatMessageList) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/SendMessageResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/SendMessageResponseDto.java new file mode 100644 index 0000000..cc9c667 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/SendMessageResponseDto.java @@ -0,0 +1,18 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class SendMessageResponseDto { + private String receivedUser; + private ChatMessageElementResponseDto message; + + public static SendMessageResponseDto of(String receivedUser, ChatMessageElementResponseDto message) { + return SendMessageResponseDto.builder() + .receivedUser(receivedUser) + .message(message) + .build(); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/UserChatResponseDto.java b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/UserChatResponseDto.java new file mode 100644 index 0000000..ef2dfaa --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/controller/dto/response/UserChatResponseDto.java @@ -0,0 +1,27 @@ +package Backend.socket.domain.chat.application.controller.dto.response; + +import Backend.socket.domain.chat.domain.ChatUser; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class UserChatResponseDto { + private String sessionId; + private String profile; + private String userName; + private String content; + private String time; + + public static UserChatResponseDto of(ChatUser user, String content, LocalDateTime time) { + return UserChatResponseDto.builder() + .sessionId(user.getSessionId()) + .profile(user.getProfile()) + .userName(user.getName()) + .content(content) + .time(time.toString()) + .build(); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/service/ChatService.java b/socket/src/main/java/Backend/socket/domain/chat/application/service/ChatService.java new file mode 100644 index 0000000..2930c28 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/service/ChatService.java @@ -0,0 +1,241 @@ +package Backend.socket.domain.chat.application.service; + +import Backend.socket.domain.chat.application.controller.dto.request.ChatListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageRequestDto; + +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageRoomRequestDto; +import Backend.socket.domain.chat.application.controller.dto.response.*; +import Backend.socket.domain.chat.domain.*; +import Backend.socket.domain.chat.repository.ChatRepository; +import Backend.socket.domain.chat.repository.RoomRepository; +import Backend.socket.domain.chat.repository.UserRepository; +import Backend.socket.global.common.image; +import Backend.socket.global.error.socketException.EntityNotFoundException; +import Backend.socket.infra.external.AwsService; +import Backend.socket.infra.external.fcm.service.PushNotificationService; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static Backend.socket.domain.chat.domain.ChatContent.createChatContent; +import static Backend.socket.global.error.ErrorCode.USER_NOT_FOUND; + + +@RequiredArgsConstructor +@Transactional +@Service +public class ChatService { + private final MongoTemplate mongoTemplate; + private final ChatRepository chatRepository; + private final UserRepository userRepository; + private final RoomRepository roomRepository; + private final AwsService awsService; + private final PushNotificationService pushNotificationService; + +// public ChatMessageResponseDto createSendMessageContent(String sessionId, ChatMessageRequestDto chatMessageRequestDto) { +// Chat chat = getChatBySessions(sessionId, chatMessageRequestDto.getChatSession()); +// User user = userRepository.findBySessionId(chatMessageRequestDto.getChatSession()).orElseThrow(); +// ChatContent chatContent = createChatContent(chatMessageRequestDto.getFromUserName(), chatMessageRequestDto.getContent(), chat); +// ChatMessageElementResponseDto chatMessage = ChatMessageElementResponseDto.of(chatContent, chatMessageRequestDto.getChatSession(), user.getProfile()); +// List sessionIdList = getSessionIdList(sessionId, chatMessageRequestDto.getChatSession()); +// saveChat(chat); +// return ChatMessageResponseDto.of(chatMessageRequestDto.getToUserName(), sessionIdList, chatMessage); +// } + public ChatMessageRoomResponseDto createSendMessageContentInRoom(String roomName, ChatMessageRoomRequestDto chatMessageRoomRequestDto) throws IOException { + StringBuilder imageBuilder = new StringBuilder(); + for (String imagePart : chatMessageRoomRequestDto.getImage()) { + imageBuilder.append(imagePart); + } + String image = imageBuilder.toString(); + String modifiedImageString = image.replaceAll("[\\[\\]]", "").replaceAll(",", " "); + System.out.println("Modified byte array: " + modifiedImageString); + Room room = getChatBySessionsInRoom(roomName, chatMessageRoomRequestDto.getChatSession()); + User user = userRepository.findBySessionId(chatMessageRoomRequestDto.getChatSession()).orElseThrow(); + String images = awsService.uploadImageToS3(modifiedImageString); + ChatContent chatContent = createChatContent(chatMessageRoomRequestDto.getFromUserName(), chatMessageRoomRequestDto.getContent(), room); + ChatMessageElementResponseDto chatMessage = ChatMessageElementResponseDto.of(chatContent, chatMessageRoomRequestDto.getChatSession(), user.getProfile(), images); + List sessionIdList = getSessionIdListInRoom(roomName, chatMessageRoomRequestDto.getChatSession()); + saveChatRoom(room); + pushNotificationService.sendChatMessageNotification(room, chatMessage, sessionIdList); //채팅 알림 + return ChatMessageRoomResponseDto.of(chatMessageRoomRequestDto.getToRoomName(), sessionIdList, chatMessage); + } + + public ChatMessageRoomResponseDto createSendImageContentInRoom(String roomName, image chatMessageRoomRequestDto) throws IOException { + // 대괄호 제거 및 공백으로 구분 + String modifiedImageString = chatMessageRoomRequestDto.getImage().replaceAll("[\\[\\]]", "").replaceAll(",", " "); + System.out.println("Modified byte array: " + modifiedImageString); + + String imageUrl = awsService.uploadImageToS3(modifiedImageString); + String images = awsService.uploadImageToS3(imageUrl); + Room room = getChatBySessionsInRoom(roomName, "113828093759900814627_ef4a27"); + User user = userRepository.findBySessionId("113828093759900814627_ef4a27").orElseThrow(); + ChatContent chatContent = createChatContent("양규리", images, room); + ChatMessageElementResponseDto chatMessage = ChatMessageElementResponseDto.of(chatContent, "113828093759900814627_ef4a27", user.getProfile(), images); + List sessionIdList = getSessionIdListInRoom(roomName, "113828093759900814627_ef4a27"); + saveChatRoom(room); + return ChatMessageRoomResponseDto.of("eksxhr", sessionIdList, chatMessage); + } + +// public ChatMessageListResponseDto sendChatDetailMessage(String sessionId, ChatMessageListRequestDto chatMessageListRequestDto) { +// Chat chat = getChatBySessions(sessionId, chatMessageListRequestDto.getChatSession()); +// ChatUserResponseDto chatUserResponseDto = getChatUserResponseDto(chat, chatMessageListRequestDto.getFromUserName()); +// List chatMessageList = ChatMessageElementResponseDto.listOf(chat.getChatContentList(), chatMessageListRequestDto.getChatSession(), null); +// saveChat(chat); +// return ChatMessageListResponseDto.of(chatUserResponseDto, chatMessageList); +// } +// +// public ChatListResponseDto sendUserChatListMessage(String sessionId, ChatListRequestDto chatListRequestDto) { +// List chatList = findChatListBySession(sessionId); +// List userChatResponseDtoList = createUserChatResponseDto(chatList, chatListRequestDto.getUserName()); +// userChatResponseDtoList.sort(Comparator.comparing(UserChatResponseDto::getTime).reversed()); +// return ChatListResponseDto.of(userChatResponseDtoList); +// } + + private List getSessionIdList(String firstSessionId, String secondSessionId) { + List sessionList = new ArrayList<>(); + sessionList.add(firstSessionId); + sessionList.add(secondSessionId); + return sessionList; + } + private List getSessionIdListInRoom(String roomName, String sessionId) { + List sessionList = new ArrayList<>(); + + // roomId를 기반으로 Room 문서 찾기 + Room room = findRoomChatByRoomName(roomName); + + if (room != null) { + // Room에 속한 모든 ChatUser의 sessionId를 리스트에 추가 + for (ChatUser chatUser : room.getChatUserList()) { + sessionList.add(chatUser.getSessionId()); + } + } + + return sessionList; + } + + private ChatUserResponseDto getChatUserResponseDto(Chat chat, String name) { + ChatUser chatUser = getChatUserReceivedUser(chat, name); + return ChatUserResponseDto.of(chatUser); + } + + private List createUserChatResponseDto(List chatList, String userName) { + List filterChat = getChatEmptyContentFilter(chatList); + return filterChat.stream() + .map(chat -> + UserChatResponseDto.of( + getChatUserReceivedUser(chat, userName), + getLastChatContent(chat.getChatContentList()).getContent(), + getLastChatContent(chat.getChatContentList()).getTime())) + .collect(Collectors.toList()); + } + + private List getChatEmptyContentFilter(List chatList) { + return chatList.stream() + .filter(chat -> (chat.getChatContentList().size() != 0)) + .collect(Collectors.toList()); + } + + private ChatUser getChatUserReceivedUser(Chat chat, String name) { + if (!Objects.equals(chat.getChatUserList().get(0).getName(), name)) + return chat.getChatUserList().get(0); + else + return chat.getChatUserList().get(1); + } + + + private ChatContent getLastChatContent(List chatContentList) { + return chatContentList.get(chatContentList.size() - 1); + } + + private Chat getChatBySessions(String firstSessionId, String secondSessionId) { + Chat chat = findFirstChatBySessions(firstSessionId, secondSessionId); + if (Objects.isNull(chat)) { + ChatUser firstChatUser = createChatUser(firstSessionId); + ChatUser secondChatUser = createChatUser(secondSessionId); + return Chat.creatChat(firstChatUser, secondChatUser); + } else + return chat; + } + private Room getChatBySessionsInRoom(String roomName, String sessionId) { + Room room = findRoomChatByRoomName(roomName); + if (Objects.isNull(room)) { + // 채팅방이 없는 경우 새로운 채팅방 생성 + room = createNewRoom(roomName); + } + + // 채팅방에 sessionId를 가진 유저가 있는지 확인 + if (!isUserExistsInRoom(room, sessionId)) { + // 유저가 없다면 새로운 유저 생성하여 채팅방에 추가 + ChatUser chatUser = createChatUser(sessionId); + room.addChatRoom(chatUser); + } + + return room; + } + + private Room createNewRoom(String roomName) { + Room room = Room.builder() + .roomName(roomName) + .build(); + return room; + } + private boolean isUserExistsInRoom(Room room, String sessionId) { + for (ChatUser chatUser : room.getChatUserList()) { + if (chatUser.getSessionId().equals(sessionId)) { + return true; + } + } + return false; + } + private ChatUser createChatUser(String sessionId) { + User user = getUserFromSessionId(sessionId); + return ChatUser.createChatUser(user); + } + + private String getReceivedUserName(Chat chat, String user) { + if (!Objects.equals(chat.getChatUserList().get(0).getName(), user)) + return chat.getChatUserList().get(0).getName(); + else + return chat.getChatUserList().get(1).getName(); + } + + private Chat findFirstChatBySessions(String firstSessionId, String secondSessionId) { + Query query = new Query(); + query.addCriteria(Criteria.where("chatUserList.sessionId").all(firstSessionId, secondSessionId)); + return mongoTemplate.findOne(query, Chat.class); + } + private Room findRoomChatByRoomName(String roomName) { + Query query = new Query(); + query.addCriteria(Criteria.where("roomName").is(roomName)); + return mongoTemplate.findOne(query, Room.class); + } + + private List findChatListBySession(String sessionId) { + Query query = new Query(); + query.addCriteria(Criteria.where("chatUserList.sessionId").all(sessionId)); + return mongoTemplate.find(query, Chat.class); + } + + private User getUserFromSessionId(String sessionId) { + return userRepository.findBySessionId(sessionId) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + } + + public void saveChat(Chat chat) { + chatRepository.save(chat); + } + public void saveChatRoom(Room room) { + roomRepository.save(room); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/service/Formatter.java b/socket/src/main/java/Backend/socket/domain/chat/application/service/Formatter.java new file mode 100644 index 0000000..a6d5180 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/service/Formatter.java @@ -0,0 +1,29 @@ +package Backend.socket.domain.chat.application.service; + +import Backend.socket.domain.chat.domain.ChatUser; +import Backend.socket.domain.chat.domain.Room; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class Formatter { + private final MongoTemplate mongoTemplate; + public ChatUser findChatUserByRoomNameAndUserName(String roomName, String userName) { + Query query = new Query(); + query.addCriteria(Criteria.where("roomName").is(roomName).and("chatUserList.name").is(userName)); + Room room = mongoTemplate.findOne(query, Room.class); + if (room != null) { + for (ChatUser chatUser : room.getChatUserList()) { + if (chatUser.getName().equals(userName)) { + return chatUser; + } + } + } + + return ChatUser.builder().build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/service/RedisSubscriber.java b/socket/src/main/java/Backend/socket/domain/chat/application/service/RedisSubscriber.java new file mode 100644 index 0000000..61ef887 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/service/RedisSubscriber.java @@ -0,0 +1,78 @@ +package Backend.socket.domain.chat.application.service; + +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageElementResponseDto; +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageResponseDto; +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageRoomResponseDto; +import Backend.socket.domain.chat.application.controller.dto.response.SendMessageResponseDto; +import Backend.socket.global.common.MessageSuccessCode; +import Backend.socket.global.common.MessageSuccessResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class RedisSubscriber implements MessageListener { + private final ObjectMapper objectMapper; + private final RedisTemplate redisTemplate; + private final SimpMessageSendingOperations messagingTemplate; + + public RedisSubscriber(ObjectMapper objectMapper, @Qualifier("redisTemplate") RedisTemplate redisTemplate, SimpMessageSendingOperations messagingTemplate){ + this.objectMapper = objectMapper; + this.redisTemplate = redisTemplate; + this.messagingTemplate = messagingTemplate; + } +// @Override +// public void onMessage(Message message, byte[] pattern) { +// String publishMessage = getPublishMessage(message); +// ChatMessageResponseDto messageResponseDto = getChatMessageFromObjectMapper(publishMessage); +// SendMessageResponseDto sendMessageResponseDto +// = SendMessageResponseDto.of(messageResponseDto.getReceivedUser(), messageResponseDto.getMessage()); +// messageResponseDto.getSessionList().forEach(sessionId -> sendChatMessage(sessionId, sendMessageResponseDto)); +// } +@Override +public void onMessage(Message message, byte[] pattern) { + String publishMessage = getPublishMessage(message); + ChatMessageRoomResponseDto messageResponseDto = getChatMessageFromObjectMapper(publishMessage); +// SendMessageResponseDto sendMessageResponseDto +// = SendMessageResponseDto.of(messageResponseDto.getReceivedUser(), messageResponseDto.getMessage()); + messageResponseDto.getSessionList().forEach(sessionId -> + sendChatMessage(messageResponseDto.getRoom(), messageResponseDto.getMessage())); +} +// private ChatMessageResponseDto getChatMessageFromObjectMapper(String publishMessage) { +// ChatMessageResponseDto messageResponseDto; +// try { +// messageResponseDto = objectMapper.readValue(publishMessage, ChatMessageResponseDto.class); +// } catch (Exception e) { +// throw new MessageDeliveryException("Error"); +// } +// return messageResponseDto; +// } +private ChatMessageRoomResponseDto getChatMessageFromObjectMapper(String publishMessage) { + ChatMessageRoomResponseDto messageResponseDto; + try { + messageResponseDto = objectMapper.readValue(publishMessage, ChatMessageRoomResponseDto.class); + } catch (Exception e) { + throw new MessageDeliveryException("Error"); + } + return messageResponseDto; +} + private String getPublishMessage(Message message) { + return (String) redisTemplate.getStringSerializer().deserialize(message.getBody()); + } + private void sendChatMessage(String roomName, ChatMessageElementResponseDto message) { + messagingTemplate.convertAndSend("/sub/room/" + roomName, + MessageSuccessResponse.of(MessageSuccessCode.RECEIVED, message)); + } +// private void sendChatMessage(String sessionId, SendMessageResponseDto publishMessage) { +// messagingTemplate.convertAndSend("/sub/chat/" + sessionId, +// MessageSuccessResponse.of(MessageSuccessCode.RECEIVED, publishMessage)); +// } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/service/RoomService.java b/socket/src/main/java/Backend/socket/domain/chat/application/service/RoomService.java new file mode 100644 index 0000000..a791376 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/service/RoomService.java @@ -0,0 +1,89 @@ +package Backend.socket.domain.chat.application.service; + +import Backend.socket.domain.chat.application.controller.dto.request.ChatMessageListRequestDto; +import Backend.socket.domain.chat.application.controller.dto.response.*; +import Backend.socket.domain.chat.domain.Chat; +import Backend.socket.domain.chat.domain.ChatContent; +import Backend.socket.domain.chat.domain.ChatUser; +import Backend.socket.domain.chat.domain.Room; +import Backend.socket.domain.chat.repository.ChatRepository; +import Backend.socket.domain.chat.repository.RoomRepository; +import Backend.socket.domain.chat.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Transactional +@Service +public class RoomService { + private final MongoTemplate mongoTemplate; + private final RoomRepository roomRepository; + private final Formatter formatter; + public RoomMessageListResponseDto sendRoomDetailMessage(String roomName) { + Room room = getChatByRoomName(roomName); + List chatUserResponseDto = getChatUserResponseDto(room); + List chatMessageList = RoomMessageElementResponseDto.listOf(room.getChatContentList(),roomName, formatter::findChatUserByRoomNameAndUserName); + saveChatRoom(room); + return RoomMessageListResponseDto.of(chatUserResponseDto, chatMessageList); + } + private List getChatUserResponseDto(Room room) { + List chatUsers = getChatUserInRoom(room); + return chatUsers.stream().map(chatUser -> ChatUserResponseDto.of(chatUser)).toList(); + } + private List getChatUserInRoom(Room room) { + //room에 있는 모든 chatuser불러오기 + return room.getChatUserList(); + } + private Room getChatByRoomName(String roomName) { + Room room = findRoomChatByRoomName(roomName); + if (Objects.isNull(room)) { + // 채팅방이 없는 경우 새로운 채팅방 생성 + room = Room.createNewRoom(roomName); + } + + + return room; + } + + private Room findRoomChatByRoomName(String roomName) { + Query query = new Query(); + query.addCriteria(Criteria.where("roomName").is(roomName)); + return mongoTemplate.findOne(query, Room.class); + } + + public void saveChatRoom(Room room) { + roomRepository.save(room); + } + public RoomListResponseDto sendUserChatListMessage(String sessionId){ + List rooms = findRoomListBySession(sessionId); + List roomChatResponseDtos = createRoomChatResponseDto(rooms); + roomChatResponseDtos.sort(Comparator.comparing(RoomChatResponseDto::getTime).reversed()); + return RoomListResponseDto.of(sessionId, roomChatResponseDtos); + } + private List createRoomChatResponseDto(List rooms) { + return rooms.stream() + .map(room -> + RoomChatResponseDto.of( + room, + getLastChatContent(room.getChatContentList()).getContent(), + getLastChatContent(room.getChatContentList()).getTime())) + .collect(Collectors.toList()); + } + private ChatContent getLastChatContent(List chatContentList) { + return chatContentList.get(chatContentList.size() - 1); + } + private List findRoomListBySession(String sessionId) { + Query query = new Query(); + query.addCriteria(Criteria.where("chatUserList.sessionId").all(sessionId)); + return mongoTemplate.find(query, Room.class); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/application/service/TriFunction.java b/socket/src/main/java/Backend/socket/domain/chat/application/service/TriFunction.java new file mode 100644 index 0000000..db2964e --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/application/service/TriFunction.java @@ -0,0 +1,6 @@ +package Backend.socket.domain.chat.application.service; + +@FunctionalInterface +public interface TriFunction { + R apply(T t, U u); +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/Chat.java b/socket/src/main/java/Backend/socket/domain/chat/domain/Chat.java new file mode 100644 index 0000000..e8e3b99 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/Chat.java @@ -0,0 +1,37 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Builder +@Document(collection = "chat") +public class Chat { + @Id + private String chatId; + @Builder.Default + private List chatUserList = new ArrayList<>(); + @Builder.Default + private List chatContentList = new ArrayList<>(); + + public static Chat creatChat(ChatUser firstUser, ChatUser secondUser) { + Chat chat = Chat.builder().build(); + chat.addChatUser(firstUser); + chat.addChatUser(secondUser); + return chat; + } + + public void addChatContent(ChatContent content) { + this.chatContentList.add(content); + } + + public void addChatUser(ChatUser chatUser) { + this.chatUserList.add(chatUser); + } + +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/ChatContent.java b/socket/src/main/java/Backend/socket/domain/chat/domain/ChatContent.java new file mode 100644 index 0000000..6fd1816 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/ChatContent.java @@ -0,0 +1,33 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class ChatContent { + private String userName; + private String content; + private LocalDateTime time; + + public static ChatContent createChatContent(String userName, String content, Chat chat) { + ChatContent chatContent = ChatContent.builder() + .userName(userName) + .content(content) + .time(LocalDateTime.now()) + .build(); + chat.addChatContent(chatContent); + return chatContent; + } + public static ChatContent createChatContent(String userName, String content, Room room) { + ChatContent chatContent = ChatContent.builder() + .userName(userName) + .content(content) + .time(LocalDateTime.now()) + .build(); + room.addChatContent(chatContent); + return chatContent; + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/ChatUser.java b/socket/src/main/java/Backend/socket/domain/chat/domain/ChatUser.java new file mode 100644 index 0000000..280b1ec --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/ChatUser.java @@ -0,0 +1,20 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ChatUser { + private String sessionId; + private String name; + private String profile; + + public static ChatUser createChatUser(User user) { + return ChatUser.builder() + .sessionId(user.getSessionId()) + .name(user.getUserName()) + .profile(user.getProfile()) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/Gender.java b/socket/src/main/java/Backend/socket/domain/chat/domain/Gender.java new file mode 100644 index 0000000..d6717d8 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/Gender.java @@ -0,0 +1,15 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum Gender { + + MAN("man"), + WOMAN("woman"), + SECRET("secret"); + + private final String gender; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/Language.java b/socket/src/main/java/Backend/socket/domain/chat/domain/Language.java new file mode 100644 index 0000000..715ea22 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/Language.java @@ -0,0 +1,13 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum Language { + KOREAN("korean"), + ENGLISH("english"); + + private final String language; +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/Platform.java b/socket/src/main/java/Backend/socket/domain/chat/domain/Platform.java new file mode 100644 index 0000000..b2a3826 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/Platform.java @@ -0,0 +1,26 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + + + +@RequiredArgsConstructor +@Getter +public enum Platform { + GOOGLE("google"), + KAKAO("kakao"), + WITHDRAW("withdraw"); + + private final String stringPlatform; + +// public static Platform getEnumPlatformFromStringPlatform(String stringPlatform) { +// return Arrays.stream(values()) +// .filter(platform -> platform.stringPlatform.equals(stringPlatform)) +// .findFirst() +// .orElseThrow(() -> new InvalidValueException(INVALID_PLATFORM_TYPE)); +// } +} + diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/Room.java b/socket/src/main/java/Backend/socket/domain/chat/domain/Room.java new file mode 100644 index 0000000..cb2a1a6 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/Room.java @@ -0,0 +1,56 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Builder +@Document(collection = "room") +public class Room { + @Id + private String roomId; + private String roomName; + private String roomSession; + private String syncName; + + @Builder.Default + private List chatUserList = new ArrayList<>(); + @Builder.Default + private List chatContentList = new ArrayList<>(); + + public static Room createRoom(List users,String roomName) { + Room room = Room.builder(). + roomName(roomName). + build(); + for(ChatUser chatUser : users){ + room.addChatRoom(chatUser); + } + return room; + } + public static Room createNewRoom(String roomName) { + Room room = Room.builder() + .roomName(roomName) + .build(); + return room; + } + public void addChatContent(ChatContent content) { + this.chatContentList.add(content); + } + + public void addChatRoom(ChatUser chatUser) { + this.chatUserList.add(chatUser); + } + public Room(String roomId, String roomName, String roomSession, String syncName, List chatUserList, List chatContentList) { + this.roomId = roomId; + this.roomName = roomName; + this.roomSession = roomSession; + this.syncName = syncName; + this.chatUserList = chatUserList != null ? chatUserList : new ArrayList<>(); + this.chatContentList = chatContentList != null ? chatContentList : new ArrayList<>(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/SyncType.java b/socket/src/main/java/Backend/socket/domain/chat/domain/SyncType.java new file mode 100644 index 0000000..dc005f2 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/SyncType.java @@ -0,0 +1,25 @@ +package Backend.socket.domain.chat.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + + +@RequiredArgsConstructor +@Getter +public enum SyncType { + + //일회성, 지속성, 내친소 + ONETIME("일회성"), + LONGTIME("지속성"), + FROM_FRIEND("내친소"); + + private final String stringSyncType; + +// public static SyncType getEnumFROMStringSyncType(String stringSyncType) { +// return Arrays.stream(values()) +// .filter(syncType -> syncType.stringSyncType.equals(stringSyncType)) +// .findFirst() +// .orElseThrow(() -> new InvalidValueException(INVALID_SYNC_TYPE)); +// } + +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/User.java b/socket/src/main/java/Backend/socket/domain/chat/domain/User.java new file mode 100644 index 0000000..e9fab53 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/User.java @@ -0,0 +1,47 @@ +package Backend.socket.domain.chat.domain; + + +import Backend.socket.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "user") +@Entity +public class User extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long id; + @Enumerated(EnumType.STRING) + private Platform platform; + @Column(unique = true) + private String platformId; + private String email; + private String userName; + private String profile; + private String refreshToken; + private String sessionId; + + @Enumerated(EnumType.STRING) + private Language language; + private String university; + private String nationality; + @Enumerated(EnumType.STRING) + private Gender gender; + + //일회성, 지속성, 내친소 + @Enumerated(EnumType.STRING) + private SyncType syncType; + + + + private String languageLevel; + //@ColumnDefault("0") + //private int sync_cnt; + +} + diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationHistory.java b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationHistory.java new file mode 100644 index 0000000..29f3526 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationHistory.java @@ -0,0 +1,56 @@ +package Backend.socket.domain.chat.domain.notification.entity; + +import Backend.socket.domain.chat.domain.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +@Table(name = "notification_history") +@Entity +public class NotificationHistory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_history_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + private String title; + + private String body; + + private String receiverToken; + + private LocalDateTime sentAt; + + private NotificationType notificationType; + + private TopCategory topCategory; + + private String infoId; + private String infoId2; + + public static NotificationHistory createHistory(User user, String title, String body, + String receiverToken, LocalDateTime sentAt, + NotificationType notificationType, TopCategory topCategory, + String infoId, String infoId2) { + return NotificationHistory.builder() + .user(user) + .title(title) + .body(body) + .receiverToken(receiverToken) + .sentAt(sentAt) + .notificationType(notificationType) + .topCategory(topCategory) + .infoId(infoId) + .infoId2(infoId2) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationType.java b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationType.java new file mode 100644 index 0000000..a0951c3 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/NotificationType.java @@ -0,0 +1,9 @@ +package Backend.socket.domain.chat.domain.notification.entity; + +public enum NotificationType { + CHAT, + CHAT_ROOM_NOTICE, + SYNC_REMINDER, + COMMENT, + REVIEW +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/TopCategory.java b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/TopCategory.java new file mode 100644 index 0000000..30643ea --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/entity/TopCategory.java @@ -0,0 +1,26 @@ +package Backend.socket.domain.chat.domain.notification.entity; + +import Backend.socket.global.error.httpException.InvalidValueException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +import static Backend.socket.global.error.ErrorCode.INVALID_NOTIFICATION_TOP_CATEGORY; + + +@RequiredArgsConstructor +@Getter +public enum TopCategory { + ACTIVITY("활동"), + MY_SYNC("내싱크"); + + private final String stringTopCategory; + + public static TopCategory getEnumTopCategoryFromStringTopCategory(String strTopCategory) { + return Arrays.stream(values()) + .filter(topCategory -> topCategory.stringTopCategory.equals(strTopCategory)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(INVALID_NOTIFICATION_TOP_CATEGORY)); + } +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/domain/notification/repository/NotificationHistoryRepository.java b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/repository/NotificationHistoryRepository.java new file mode 100644 index 0000000..dcae7bb --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/domain/notification/repository/NotificationHistoryRepository.java @@ -0,0 +1,12 @@ +package Backend.socket.domain.chat.domain.notification.repository; + +import Backend.socket.domain.chat.domain.notification.entity.NotificationHistory; +import Backend.socket.domain.chat.domain.notification.entity.TopCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NotificationHistoryRepository extends JpaRepository { +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/repository/ChatRepository.java b/socket/src/main/java/Backend/socket/domain/chat/repository/ChatRepository.java new file mode 100644 index 0000000..45c4c41 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/repository/ChatRepository.java @@ -0,0 +1,8 @@ +package Backend.socket.domain.chat.repository; + + +import Backend.socket.domain.chat.domain.Chat; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface ChatRepository extends MongoRepository { +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/repository/RoomRepository.java b/socket/src/main/java/Backend/socket/domain/chat/repository/RoomRepository.java new file mode 100644 index 0000000..163fdbe --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/repository/RoomRepository.java @@ -0,0 +1,7 @@ +package Backend.socket.domain.chat.repository; + +import Backend.socket.domain.chat.domain.Room; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface RoomRepository extends MongoRepository { +} diff --git a/socket/src/main/java/Backend/socket/domain/chat/repository/UserRepository.java b/socket/src/main/java/Backend/socket/domain/chat/repository/UserRepository.java new file mode 100644 index 0000000..47ad8d4 --- /dev/null +++ b/socket/src/main/java/Backend/socket/domain/chat/repository/UserRepository.java @@ -0,0 +1,10 @@ +package Backend.socket.domain.chat.repository; + +import Backend.socket.domain.chat.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findBySessionId(String sessionId); +} diff --git a/socket/src/main/java/Backend/socket/global/common/AuthenticationInterceptor.java b/socket/src/main/java/Backend/socket/global/common/AuthenticationInterceptor.java new file mode 100644 index 0000000..5360a41 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/AuthenticationInterceptor.java @@ -0,0 +1,66 @@ +package Backend.socket.global.common; + +import Backend.socket.global.error.socketException.UnauthorizedException; +import Backend.socket.infra.config.auth.UserAuthentication; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +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.messaging.support.MessageHeaderAccessor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Collections; + +import static Backend.socket.global.error.ErrorCode.INVALID_ACCESS_TOKEN_VALUE; + + +@Slf4j +@RequiredArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE + 50) +@Component +public class AuthenticationInterceptor implements ChannelInterceptor { + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + private final JwtProvider jwtProvider; + + @Override + public Message preSend(Message message, MessageChannel channel) { +// StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); +// StompCommand command = accessor.getCommand(); +// if (!command.equals(StompCommand.CONNECT)) +// return message; +// String accessToken = getAccessTokenFromHeader(accessor); +// validateJwtAccessToken(accessToken); +// Long userId = getUserIdFromAccessToken(accessToken); +// setAuthentication(accessor, userId); + return message; + } + + private String getAccessTokenFromHeader(StompHeaderAccessor accessor) { + String accessToken = String.valueOf(accessor.getFirstNativeHeader(AUTHORIZATION)); + if (!(StringUtils.hasText(accessToken) && accessToken.startsWith(BEARER))) + throw new UnauthorizedException(INVALID_ACCESS_TOKEN_VALUE); + return accessToken.substring(BEARER.length()); + } + + private void setAuthentication(StompHeaderAccessor headerAccessor, Long userId) { + UsernamePasswordAuthenticationToken authentication = new UserAuthentication(userId, null, Collections.singleton((GrantedAuthority) () -> AUTHORIZATION)); + headerAccessor.setUser(authentication); + } + + private Long getUserIdFromAccessToken(String accessToken) { + return jwtProvider.getSubject(accessToken); + } + + private void validateJwtAccessToken(String accessToken) { + jwtProvider.validateAccessToken(accessToken); + } +} diff --git a/socket/src/main/java/Backend/socket/global/common/BaseEntity.java b/socket/src/main/java/Backend/socket/global/common/BaseEntity.java new file mode 100644 index 0000000..82cef39 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/BaseEntity.java @@ -0,0 +1,27 @@ +package Backend.socket.global.common; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@Getter +@Setter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/socket/src/main/java/Backend/socket/global/common/HealthCheck.java b/socket/src/main/java/Backend/socket/global/common/HealthCheck.java new file mode 100644 index 0000000..3150fa3 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/HealthCheck.java @@ -0,0 +1,100 @@ +package Backend.socket.global.common; + +import Backend.socket.infra.external.AwsService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Base64; +@RestController +@RequiredArgsConstructor +@RequestMapping +public class HealthCheck { + private final AwsService awsService; + @GetMapping("/") + public String MeetUpServer() { + return "test"; + } + @GetMapping("/image") + public String uploadImage(@RequestParam(name = "image") String image) throws IOException { + String[] strings = image.split(" "); // ","을 기준으로 바이트 코드를 나눠준다 + String base64Image = strings[1]; + String extension = ""; // if 문을 통해 확장자명을 정해줌 + if (strings[0].equals("data:image/jpeg;base64")) { + extension = "jpeg"; + } else if (strings[0].equals("data:image/png;base64")){ + extension = "png"; + } else { + extension = "jpg"; + } + + +// ... + + byte[] imageBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image); // 바이트 코드를 // 바이트 코드를 + + File tempFile = File.createTempFile("image", "." + extension); // createTempFile을 통해 임시 파일을 생성해준다. (임시파일은 지워줘야함) + try (OutputStream outputStream = new FileOutputStream(tempFile)) { + outputStream.write(imageBytes); // OutputStream outputStream = new FileOutputStream(tempFile)을 통해 생성한 outputStream 객체에 imageBytes를 작성해준다. + } + // 문자열을 공백을 기준으로 분리하여 문자열 배열로 변환 + String[] byteStrings = image.split(" "); + + // byte 배열 생성 + byte[] imageData = new byte[byteStrings.length]; + + for (int i = 0; i < byteStrings.length; i++) { + if (byteStrings[i].matches("-?[0-9]+")) { + imageData[i] = Byte.parseByte(byteStrings[i]); + } else if (byteStrings[i].matches("-?0x[0-9a-fA-F]+")) { + imageData[i] = (byte) Integer.parseInt(byteStrings[i].substring(2), 16); + } else { + // 잘못된 형식의 문자열인 경우 처리할 작업 + imageData[i] = 0; + } + } + + // 변환된 byte 배열을 사용하여 이미지 업로드 +// String imageUrl = awsService.uploadImageToS3(imageBytes); + return null; + } + @PostMapping("/images") + public List uploadImages(@RequestBody List imageDataList) { + List imageUrls = awsService.uploadImages(imageDataList); + return imageUrls; + } + + @PostMapping("/test") + public String uploadImagea(@RequestBody String image) throws IOException { + int size = image.length(); + // 대괄호 제거 및 공백으로 구분 + String modifiedImageString = image.replaceAll("[\\[\\]]", "").replaceAll(",", " "); +// System.out.println("Modified byte array: " + modifiedImageString); + + String imageUrl = awsService.uploadImageToS3(modifiedImageString); + return imageUrl; + } + + @GetMapping("/image/byte") + public ResponseEntity uploadImage(@RequestBody byte[] image) throws IOException { + im + // Base64 인코딩 + String base64EncodedString = Base64.getEncoder().encodeToString(image); + int encodedSize = base64EncodedString.length(); + System.out.println("Base64 Encoded String: " + base64EncodedString); + System.out.println("Encoded size: " + encodedSize + " bytes"); + + // AWS S3 업로드 + String imageUrl = awsService.uploadImageToS3(base64EncodedString); + + return ResponseEntity.ok(imageUrl); + } +} diff --git a/socket/src/main/java/Backend/socket/global/common/JwtProvider.java b/socket/src/main/java/Backend/socket/global/common/JwtProvider.java new file mode 100644 index 0000000..f0e1e67 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/JwtProvider.java @@ -0,0 +1,57 @@ +package Backend.socket.global.common; + +import Backend.socket.global.error.socketException.UnauthorizedException; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Base64; + +import static Backend.socket.global.error.ErrorCode.EXPIRED_ACCESS_TOKEN; +import static Backend.socket.global.error.ErrorCode.INVALID_ACCESS_TOKEN_VALUE; + + +@Getter +@Component +public class JwtProvider { + @Value("${jwt.secret}") + private String secretKey; + @Value("${jwt.access-token-expire-time}") + private long ACCESS_TOKEN_EXPIRE_TIME; + @Value("${jwt.refresh-token-expire-time}") + private long REFRESH_TOKEN_EXPIRE_TIME; + + public void validateAccessToken(String accessToken) { + try { + getJwtParser().parseClaimsJws(accessToken); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(EXPIRED_ACCESS_TOKEN); + } catch (Exception e) { + e.printStackTrace(); + throw new UnauthorizedException(INVALID_ACCESS_TOKEN_VALUE); + } + } + + public Long getSubject(String token) { + return Long.valueOf(getJwtParser().parseClaimsJws(token) + .getBody() + .getSubject()); + } + + private JwtParser getJwtParser() { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build(); + } + + private Key getSigningKey() { + String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); + return Keys.hmacShaKeyFor(encoded.getBytes()); + } +} + diff --git a/socket/src/main/java/Backend/socket/global/common/MessageSuccessCode.java b/socket/src/main/java/Backend/socket/global/common/MessageSuccessCode.java new file mode 100644 index 0000000..c3c3083 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/MessageSuccessCode.java @@ -0,0 +1,17 @@ +package Backend.socket.global.common; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum MessageSuccessCode { + RECEIVED(200, "received"), + MESSAGE(200, "messageDetail"), + CHATLIST(200, "chatList"), + SEARCH(200, "search"); + + private final int code; + private final String messageType; +} diff --git a/socket/src/main/java/Backend/socket/global/common/MessageSuccessResponse.java b/socket/src/main/java/Backend/socket/global/common/MessageSuccessResponse.java new file mode 100644 index 0000000..e7ee808 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/MessageSuccessResponse.java @@ -0,0 +1,23 @@ +package Backend.socket.global.common; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class MessageSuccessResponse { + private int code; + private String messageType; + private T data; + + public static MessageSuccessResponse of(MessageSuccessCode successCode, T data) { + return MessageSuccessResponse.builder() + .code(successCode.getCode()) + .messageType(successCode.getMessageType()) + .data(data) + .build(); + } +} diff --git a/socket/src/main/java/Backend/socket/global/common/image.java b/socket/src/main/java/Backend/socket/global/common/image.java new file mode 100644 index 0000000..e239804 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/image.java @@ -0,0 +1,11 @@ +package Backend.socket.global.common; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class image { + private String image; +} diff --git a/socket/src/main/java/Backend/socket/global/common/imageList.java b/socket/src/main/java/Backend/socket/global/common/imageList.java new file mode 100644 index 0000000..cbecacd --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/common/imageList.java @@ -0,0 +1,4 @@ +package Backend.socket.global.common; + +public class imageList { +} diff --git a/socket/src/main/java/Backend/socket/global/error/ErrorCode.java b/socket/src/main/java/Backend/socket/global/error/ErrorCode.java new file mode 100644 index 0000000..d88c4f6 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/ErrorCode.java @@ -0,0 +1,42 @@ +package Backend.socket.global.error; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum ErrorCode { + INVALID_IMAGE_TYPE(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일입니다."), + /** + * 401 Unauthorized + */ + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "리소스 접근 권한이 없습니다."), + INVALID_ACCESS_TOKEN_VALUE(HttpStatus.UNAUTHORIZED, "액세스 토큰의 값이 올바르지 않습니다."), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰이 만료되었습니다. 재발급 받아주세요."), + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요."), + INVALID_ROADMAP_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 로드맵 타입입니다."), + INVALID_TEMPLATE_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 회의록 타입입니다."), + INVALID_USER_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 유저 타입니다."), + INVALID_NOTIFICATION_TOP_CATEGORY(HttpStatus.BAD_REQUEST, "유효하지 않은 알림 탑카테고리입니다."), + + /** + * 404 Not Found + */ + CHATTING_NOT_FOUND(HttpStatus.NOT_FOUND, "채팅 정보를 찾을 수 없습니다."), + MESSAGE_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "모임 정보를 찾을 수 없습니다."), + SEARCH_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "검색 종류를 찾을 수 없습니다."), + SUB_SEARCH_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "세부 검색 종류를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."), + + /** + * 500 Internal Server Error + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), + S3_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다."); + + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/socket/src/main/java/Backend/socket/global/error/dto/ErrorBaseResponse.java b/socket/src/main/java/Backend/socket/global/error/dto/ErrorBaseResponse.java new file mode 100644 index 0000000..e279485 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/dto/ErrorBaseResponse.java @@ -0,0 +1,23 @@ +package Backend.socket.global.error.dto; + +import Backend.socket.global.error.ErrorCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class ErrorBaseResponse { + private int status; + private String message; + + public static ErrorBaseResponse of(ErrorCode errorCode) { + return ErrorBaseResponse.builder() + .status(errorCode.getHttpStatus().value()) + .message(errorCode.getMessage()) + .build(); + } +} + diff --git a/socket/src/main/java/Backend/socket/global/error/handler/MessageErrorHandler.java b/socket/src/main/java/Backend/socket/global/error/handler/MessageErrorHandler.java new file mode 100644 index 0000000..94a92a3 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/handler/MessageErrorHandler.java @@ -0,0 +1,36 @@ +package Backend.socket.global.error.handler; + +import Backend.socket.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.Message; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@RequiredArgsConstructor +@Component +public class MessageErrorHandler extends StompSubProtocolErrorHandler { + @Override + public Message handleClientMessageProcessingError(Message clientMessage, Throwable ex) { + return super.handleClientMessageProcessingError(clientMessage, ex); + } + + private Message errorMessage(ErrorCode errorCode) { + String code = String.valueOf(errorCode.getMessage()); + StompHeaderAccessor accessor = getStompHeaderAccessor(errorCode); + return MessageBuilder.createMessage(code.getBytes(StandardCharsets.UTF_8), accessor.getMessageHeaders()); + } + + private StompHeaderAccessor getStompHeaderAccessor(ErrorCode errorCode) { + StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR); + accessor.setMessage(String.valueOf(errorCode.getMessage())); + accessor.setLeaveMutable(true); + return accessor; + } +} diff --git a/socket/src/main/java/Backend/socket/global/error/httpException/BusinessException.java b/socket/src/main/java/Backend/socket/global/error/httpException/BusinessException.java new file mode 100644 index 0000000..842e1eb --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/httpException/BusinessException.java @@ -0,0 +1,20 @@ +package Backend.socket.global.error.httpException; + + +import Backend.socket.global.error.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/socket/src/main/java/Backend/socket/global/error/httpException/InternalServerException.java b/socket/src/main/java/Backend/socket/global/error/httpException/InternalServerException.java new file mode 100644 index 0000000..8336d06 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/httpException/InternalServerException.java @@ -0,0 +1,10 @@ +package Backend.socket.global.error.httpException; + + +import Backend.socket.global.error.ErrorCode; + +public class InternalServerException extends BusinessException { + public InternalServerException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/socket/src/main/java/Backend/socket/global/error/httpException/InvalidValueException.java b/socket/src/main/java/Backend/socket/global/error/httpException/InvalidValueException.java new file mode 100644 index 0000000..c0d5a0a --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/httpException/InvalidValueException.java @@ -0,0 +1,10 @@ +package Backend.socket.global.error.httpException; + + +import Backend.socket.global.error.ErrorCode; + +public class InvalidValueException extends BusinessException { + public InvalidValueException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/socket/src/main/java/Backend/socket/global/error/httpException/UnauthorizedException.java b/socket/src/main/java/Backend/socket/global/error/httpException/UnauthorizedException.java new file mode 100644 index 0000000..8bea7fa --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/httpException/UnauthorizedException.java @@ -0,0 +1,11 @@ +package Backend.socket.global.error.httpException; + + +import Backend.socket.global.error.ErrorCode; + +public class UnauthorizedException extends BusinessException { + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} + diff --git a/socket/src/main/java/Backend/socket/global/error/socketException/BusinessException.java b/socket/src/main/java/Backend/socket/global/error/socketException/BusinessException.java new file mode 100644 index 0000000..5063cfd --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/socketException/BusinessException.java @@ -0,0 +1,15 @@ +package Backend.socket.global.error.socketException; + +import Backend.socket.global.error.ErrorCode; +import lombok.Getter; +import org.springframework.messaging.MessageDeliveryException; + +@Getter +public class BusinessException extends MessageDeliveryException { + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/socket/src/main/java/Backend/socket/global/error/socketException/EntityNotFoundException.java b/socket/src/main/java/Backend/socket/global/error/socketException/EntityNotFoundException.java new file mode 100644 index 0000000..9d2cefe --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/socketException/EntityNotFoundException.java @@ -0,0 +1,9 @@ +package Backend.socket.global.error.socketException; + +import Backend.socket.global.error.ErrorCode; + +public class EntityNotFoundException extends BusinessException { + public EntityNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/socket/src/main/java/Backend/socket/global/error/socketException/UnauthorizedException.java b/socket/src/main/java/Backend/socket/global/error/socketException/UnauthorizedException.java new file mode 100644 index 0000000..7994257 --- /dev/null +++ b/socket/src/main/java/Backend/socket/global/error/socketException/UnauthorizedException.java @@ -0,0 +1,10 @@ +package Backend.socket.global.error.socketException; + + +import Backend.socket.global.error.ErrorCode; + +public class UnauthorizedException extends BusinessException { + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/socket/src/main/java/Backend/socket/infra/config/AgentWebSocketHandlerDecoratorFactory.java b/socket/src/main/java/Backend/socket/infra/config/AgentWebSocketHandlerDecoratorFactory.java new file mode 100644 index 0000000..b009ed7 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/AgentWebSocketHandlerDecoratorFactory.java @@ -0,0 +1,21 @@ +package Backend.socket.infra.config; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory; + +public class AgentWebSocketHandlerDecoratorFactory implements WebSocketHandlerDecoratorFactory { + @Override + public WebSocketHandler decorate(WebSocketHandler handler) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTargetClass(AopUtils.getTargetClass(handler)); + proxyFactory.setTargetSource(new SingletonTargetSource(handler)); + proxyFactory.addAdvisor(new DefaultIntroductionAdvisor(new SubProtocolWebSocketHandlerInterceptor())); + proxyFactory.setOptimize(true); + proxyFactory.setExposeProxy(true); + return (WebSocketHandler) proxyFactory.getProxy(); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/infra/config/AwsS3Config.java b/socket/src/main/java/Backend/socket/infra/config/AwsS3Config.java new file mode 100644 index 0000000..b3eafec --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/AwsS3Config.java @@ -0,0 +1,30 @@ +package Backend.socket.infra.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsS3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + @Bean + public AmazonS3Client amazonS3Client() { + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey))) + .build(); + } + + +} diff --git a/socket/src/main/java/Backend/socket/infra/config/FCMConfig.java b/socket/src/main/java/Backend/socket/infra/config/FCMConfig.java new file mode 100644 index 0000000..08fa816 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/FCMConfig.java @@ -0,0 +1,38 @@ +package Backend.socket.infra.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +@Configuration +public class FCMConfig { + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("firebase/AccountKey.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/socket/src/main/java/Backend/socket/infra/config/MongoDbConfig.java b/socket/src/main/java/Backend/socket/infra/config/MongoDbConfig.java new file mode 100644 index 0000000..2e77449 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/MongoDbConfig.java @@ -0,0 +1,30 @@ +package Backend.socket.infra.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.config.EnableMongoAuditing; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +@RequiredArgsConstructor +@EnableMongoAuditing +@EnableMongoRepositories(basePackages = "Backend.socket.domain") +@Configuration +public class MongoDbConfig { + private final MongoMappingContext mongoMappingContext; + + @Bean + public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory mongoDatabaseFactory, + MongoMappingContext mongoMappingContext) { + DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory); + MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext); + converter.setTypeMapper(new DefaultMongoTypeMapper(null)); + return converter; + } +} diff --git a/socket/src/main/java/Backend/socket/infra/config/RedisConfig.java b/socket/src/main/java/Backend/socket/infra/config/RedisConfig.java new file mode 100644 index 0000000..74b5ee0 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/RedisConfig.java @@ -0,0 +1,104 @@ +package Backend.socket.infra.config; + +import Backend.socket.domain.chat.application.service.RedisSubscriber; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Value("${spring.redis.chat.host}") + private String chatRedisHost; + + @Value("${spring.redis.chat.port}") + private int chatRedisPort; + + @Value("${spring.redis.chat.password}") + private String chatRedisPassword; + + @Value("${spring.redis.fcm.host}") + private String fcmRedisHost; + + @Value("${spring.redis.fcm.port}") + private int fcmRedisPort; + + @Value("${spring.redis.fcm.password}") + private String fcmRedisPassword; + + @Bean + public RedisTemplate redisTemplate(@Qualifier("chatRedisConnectionFactory") RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + return template; + } + @Bean + @Qualifier("chatRedisConnectionFactory") + public RedisConnectionFactory chatRedisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(chatRedisHost); + config.setPort(chatRedisPort); + config.setPassword(chatRedisPassword); + return new LettuceConnectionFactory(config); + } + + @Bean + @Qualifier("fcmRedisConnectionFactory") + public RedisConnectionFactory fcmRedisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); + config.setHostName(fcmRedisHost); + config.setPort(fcmRedisPort); + config.setPassword(fcmRedisPassword); + return new LettuceConnectionFactory(config); + } + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + @Qualifier("chatRedisConnectionFactory") RedisConnectionFactory connectionFactory, + MessageListenerAdapter listenerAdapter, + ChannelTopic channelTopic) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(listenerAdapter, channelTopic); + return container; + } + + @Bean + public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) { + return new MessageListenerAdapter(subscriber, "onMessage"); + } + + @Bean + public RedisTemplate chatRedisTemplate( + @Qualifier("chatRedisConnectionFactory") RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); + return redisTemplate; + } + + @Bean + public RedisTemplate fcmRedisTemplate( + @Qualifier("fcmRedisConnectionFactory") RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); + return redisTemplate; + } + + @Bean + public ChannelTopic channelTopic() { + return new ChannelTopic("meetingRoom"); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/infra/config/SocketConfig.java b/socket/src/main/java/Backend/socket/infra/config/SocketConfig.java new file mode 100644 index 0000000..604e7b6 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/SocketConfig.java @@ -0,0 +1,52 @@ +package Backend.socket.infra.config; + +import Backend.socket.global.common.AuthenticationInterceptor; +import Backend.socket.global.error.handler.MessageErrorHandler; +import Backend.socket.infra.config.auth.UserIdArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +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; + +import java.util.List; + +@RequiredArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE + 50) +@EnableWebSocketMessageBroker +@Configuration +public class SocketConfig implements WebSocketMessageBrokerConfigurer { + private final AuthenticationInterceptor authenticationInterceptor; + private final MessageErrorHandler messageErrorHandler; + private final UserIdArgumentResolver userIdArgumentResolver; + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/sub"); + registry.setApplicationDestinationPrefixes("/pub"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { +// registry.addEndpoint("/ws").setAllowedOrigins("*"); + registry.addEndpoint("/ws"); +// .setAllowedOrigins("*") // 프론트엔드의 도메인을 허용 +// .withSockJS(); + registry.setErrorHandler(messageErrorHandler); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { +// registration.interceptors(authenticationInterceptor); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(userIdArgumentResolver); + } +} diff --git a/socket/src/main/java/Backend/socket/infra/config/SocketSecurityConfig.java b/socket/src/main/java/Backend/socket/infra/config/SocketSecurityConfig.java new file mode 100644 index 0000000..0310ff5 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/SocketSecurityConfig.java @@ -0,0 +1,32 @@ +package Backend.socket.infra.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; +import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer; + +import static org.springframework.messaging.simp.SimpMessageType.MESSAGE; +import static org.springframework.messaging.simp.SimpMessageType.SUBSCRIBE; + +@Configuration +public class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { + + @Override + protected boolean sameOriginDisabled() { + return true; + } + + @Override + protected void configureInbound(MessageSecurityMetadataSourceRegistry message) { + message + .nullDestMatcher().permitAll() + .simpDestMatchers("/pub/**").permitAll() + .simpSubscribeDestMatchers("/sub/**").permitAll() + .anyMessage().permitAll(); +// .nullDestMatcher().permitAll() +// .simpDestMatchers("/pub/**").authenticated() +// .simpSubscribeDestMatchers("/sub/**").authenticated() +// .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() +// .anyMessage().denyAll(); + } +} + diff --git a/socket/src/main/java/Backend/socket/infra/config/SubProtocolWebSocketHandlerInterceptor.java b/socket/src/main/java/Backend/socket/infra/config/SubProtocolWebSocketHandlerInterceptor.java new file mode 100644 index 0000000..0b9fd82 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/SubProtocolWebSocketHandlerInterceptor.java @@ -0,0 +1,16 @@ +package Backend.socket.infra.config; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.web.socket.WebSocketSession; + +public class SubProtocolWebSocketHandlerInterceptor extends DelegatingIntroductionInterceptor { + @Override + protected Object doProceed(MethodInvocation mi) throws Throwable { + if (mi.getMethod().getName().equals("afterConnectionEstablished")) { + WebSocketSession session = (WebSocketSession) mi.getArguments()[0]; + session.setTextMessageSizeLimit(50 * 1024 * 1024); + } + return super.doProceed(mi); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/infra/config/WebConfig.java b/socket/src/main/java/Backend/socket/infra/config/WebConfig.java new file mode 100644 index 0000000..0207633 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/WebConfig.java @@ -0,0 +1,25 @@ +//package Backend.socket.infra.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.servlet.config.annotation.CorsRegistry; +//import org.springframework.web.servlet.config.annotation.EnableWebMvc; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +//@EnableWebMvc +//@Configuration +//public class WebConfig implements WebMvcConfigurer { +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/**") +// .allowedOrigins("*") +// .allowedMethods("*") +// .exposedHeaders("Access-Control-Allow-Origin", +// "Access-Control-Allow-Methods", +// "Access-Control-Allow-Headers", +// "Access-Control-Max-Age", +// "Access-Control-Request-Headers", +// "Access-Control-Request-Method") +// .allowCredentials(false) +// .maxAge(30000000); +// } +//} diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/ExceptionHandlerFilter.java b/socket/src/main/java/Backend/socket/infra/config/auth/ExceptionHandlerFilter.java new file mode 100644 index 0000000..a024486 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/ExceptionHandlerFilter.java @@ -0,0 +1,53 @@ +package Backend.socket.infra.config.auth; + +import Backend.socket.global.error.dto.ErrorBaseResponse; +import Backend.socket.global.error.httpException.InvalidValueException; +import Backend.socket.global.error.socketException.UnauthorizedException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static Backend.socket.global.error.ErrorCode.INTERNAL_SERVER_ERROR; + + +@Slf4j +public class ExceptionHandlerFilter extends OncePerRequestFilter { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (UnauthorizedException e) { + handleUnauthorizedException(response, e); + } catch (Exception ee) { + handleException(response); + } + } + + private void handleUnauthorizedException(HttpServletResponse response, Exception e) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + if (e instanceof UnauthorizedException ue) { + response.setStatus(ue.getErrorCode().getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(ue.getErrorCode()))); + } else if (e instanceof InvalidValueException ie) { + response.setStatus(ie.getErrorCode().getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(ie.getErrorCode()))); + } + } + + private void handleException(HttpServletResponse response) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + response.setStatus(INTERNAL_SERVER_ERROR.getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(INTERNAL_SERVER_ERROR))); + } +} diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/IgnorePathConsts.java b/socket/src/main/java/Backend/socket/infra/config/auth/IgnorePathConsts.java new file mode 100644 index 0000000..e08220e --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/IgnorePathConsts.java @@ -0,0 +1,29 @@ +package Backend.socket.infra.config.auth; + + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpMethod; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class IgnorePathConsts { + + private static final Map> ignorePathMap= + Map.of( + "/**", Set.of(HttpMethod.GET), + "/swagger-ui/**", Set.of(HttpMethod.GET, HttpMethod.OPTIONS) + ); + + public static Boolean isIgnorablePath(String uri, HttpMethod httpMethod){ + if(ignorePathMap.containsKey(uri)){ + Set methods = ignorePathMap.get(uri); + return methods.stream() + .anyMatch(method -> method.equals(httpMethod)); + } + return false; + } +} diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationEntryPoint.java b/socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..58d1fce --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationEntryPoint.java @@ -0,0 +1,32 @@ +package Backend.socket.infra.config.auth; + +import Backend.socket.global.error.dto.ErrorBaseResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static Backend.socket.global.error.ErrorCode.UNAUTHORIZED; + + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + handleException(response); + } + + private void handleException(HttpServletResponse response) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("utf-8"); + response.setStatus(UNAUTHORIZED.getHttpStatus().value()); + response.getWriter().write(objectMapper.writeValueAsString(ErrorBaseResponse.of(UNAUTHORIZED))); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationFilter.java b/socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..ea88c21 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/JwtAuthenticationFilter.java @@ -0,0 +1,87 @@ +package Backend.socket.infra.config.auth; + +import Backend.socket.global.common.JwtProvider; +import Backend.socket.global.error.socketException.UnauthorizedException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static Backend.socket.global.error.ErrorCode.INVALID_ACCESS_TOKEN; + + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION = "Authorization"; + private static final String BEARER = "Bearer "; + private final JwtProvider jwtProvider; + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + //url이 통과해도되는지 체크 (http 메서드와 함께 자료구조에 저장하여 검증) + String url = request.getRequestURI(); + String method = request.getMethod(); + + if(!IgnorePathConsts.isIgnorablePath(url, HttpMethod.valueOf(method))){ +// //위가 통과되면 토큰을 검출 +// String authorization = request.getHeader("Authorization"); +// +// /*//Authorization 헤더 검증 +// if (authorization == null || !authorization.startsWith("Bearer ")) { +// +// System.out.println("token null"); +// filterChain.doFilter(request, response); //doFilter를 통해 request와 response를 +// +// }*/ +// +// System.out.println("authorization now"); +// //Bearer 부분 제거 후 순수 토큰만 획득 +// String token = authorization.split(" ")[1]; +// +// //토큰이 유효한지 검증, 유효성 검증은 extract 메서드에서 처리 +// Long userId = jwtUtil.extractUserClaim(token).getUserId(); +// +// Authentication authToken = new JwtAuthentication(userId); +// +// //세션에 사용자 등록 +// SecurityContextHolder.getContext().setAuthentication(authToken); + } + + + + filterChain.doFilter(request, response); + } + +// @Override +// protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { +// final String accessToken = getAccessTokenFromHttpServletRequest(request); +// jwtProvider.validateAccessToken(accessToken); +// final Long userId = jwtProvider.getSubject(accessToken); +// setAuthentication(request, userId); +// filterChain.doFilter(request, response); +// } + + + private String getAccessTokenFromHttpServletRequest(HttpServletRequest request) { + String accessToken = request.getHeader(AUTHORIZATION); + if (StringUtils.hasText(accessToken) && accessToken.startsWith(BEARER)) { + return accessToken.substring(BEARER.length()); + } + throw new UnauthorizedException(INVALID_ACCESS_TOKEN); + } + + private void setAuthentication(HttpServletRequest request, Long userId) { + UsernamePasswordAuthenticationToken authentication = new UserAuthentication(userId, null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/SecurityConfig.java b/socket/src/main/java/Backend/socket/infra/config/auth/SecurityConfig.java new file mode 100644 index 0000000..f6d6bbd --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/SecurityConfig.java @@ -0,0 +1,65 @@ +package Backend.socket.infra.config.auth; + +import Backend.socket.global.common.JwtProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@RequiredArgsConstructor +@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableWebSecurity +@Configuration +public class SecurityConfig { + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtProvider jwtProvider; + private static final String[] whiteList = {"/**"}; + +// @Bean +// public WebSecurityCustomizer webSecurityCustomizer() { +// return web -> web.ignoring().requestMatchers(whiteList); +// } + +// @Bean +// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { +// return http +// .formLogin(AbstractHttpConfigurer::disable) +// .httpBasic(AbstractHttpConfigurer::disable) +// .csrf(AbstractHttpConfigurer::disable) +// .sessionManagement(sessionManagementConfigurer -> +// sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) +// .exceptionHandling(exceptionHandlingConfigurer -> +// exceptionHandlingConfigurer.authenticationEntryPoint(jwtAuthenticationEntryPoint)) +// .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> +// authorizationManagerRequestMatcherRegistry.anyRequest().authenticated()) +// .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) +// .addFilterBefore(new ExceptionHandlerFilter(), JwtAuthenticationFilter.class) +// .build(); +// } + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors(AbstractHttpConfigurer::disable); + + http.csrf(AbstractHttpConfigurer::disable); + + http.authorizeHttpRequests(request ->{ + request.anyRequest().permitAll(); + }); + + http.formLogin(AbstractHttpConfigurer::disable); + http.httpBasic(AbstractHttpConfigurer::disable); + http.logout(AbstractHttpConfigurer::disable); + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + return http.build(); + } + +} diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/UserAuthentication.java b/socket/src/main/java/Backend/socket/infra/config/auth/UserAuthentication.java new file mode 100644 index 0000000..4954265 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/UserAuthentication.java @@ -0,0 +1,12 @@ +package Backend.socket.infra.config.auth; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } +} diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/UserId.java b/socket/src/main/java/Backend/socket/infra/config/auth/UserId.java new file mode 100644 index 0000000..49c5466 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/UserId.java @@ -0,0 +1,11 @@ +package Backend.socket.infra.config.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserId { +} diff --git a/socket/src/main/java/Backend/socket/infra/config/auth/UserIdArgumentResolver.java b/socket/src/main/java/Backend/socket/infra/config/auth/UserIdArgumentResolver.java new file mode 100644 index 0000000..848f0fe --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/config/auth/UserIdArgumentResolver.java @@ -0,0 +1,24 @@ +package Backend.socket.infra.config.auth; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class UserIdArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasUserIdAnnotation = parameter.hasParameterAnnotation(UserId.class); + boolean hasLongType = Long.class.isAssignableFrom(parameter.getParameterType()); + return hasUserIdAnnotation && hasLongType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + return SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + } +} diff --git a/socket/src/main/java/Backend/socket/infra/external/AwsService.java b/socket/src/main/java/Backend/socket/infra/external/AwsService.java new file mode 100644 index 0000000..3c7f0b4 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/external/AwsService.java @@ -0,0 +1,143 @@ +package Backend.socket.infra.external; + + +import Backend.socket.global.error.httpException.InternalServerException; +import Backend.socket.global.error.httpException.InvalidValueException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static Backend.socket.global.error.ErrorCode.INVALID_IMAGE_TYPE; +import static Backend.socket.global.error.ErrorCode.S3_UPLOAD_ERROR; + + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class AwsService { + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + + public String uploadImageToS3(String image) throws IOException { + String[] strings = image.split(" "); // ","을 기준으로 바이트 코드를 나눠준다 + String base64Image = strings[1]; + String extension = ""; // if 문을 통해 확장자명을 정해줌 + if (strings[0].equals("data:image/jpeg;base64")) { + extension = "jpeg"; + } else if (strings[0].equals("data:image/png;base64")){ + extension = "png"; + } else { + extension = "jpg"; + } + + +// ... + + byte[] imageBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image); // 바이트 코드를 // 바이트 코드를 + + File tempFile = File.createTempFile("image", "." + extension); // createTempFile을 통해 임시 파일을 생성해준다. (임시파일은 지워줘야함) + try (OutputStream outputStream = new FileOutputStream(tempFile)) { + outputStream.write(imageBytes); // OutputStream outputStream = new FileOutputStream(tempFile)을 통해 생성한 outputStream 객체에 imageBytes를 작성해준다. + } + // 문자열을 공백을 기준으로 분리하여 문자열 배열로 변환 + String[] byteStrings = image.split(" "); + + // byte 배열 생성 + byte[] imageData = new byte[byteStrings.length]; + + for (int i = 0; i < byteStrings.length; i++) { + if (byteStrings[i].matches("-?[0-9]+")) { + imageData[i] = Byte.parseByte(byteStrings[i]); + } else if (byteStrings[i].matches("-?0x[0-9a-fA-F]+")) { + imageData[i] = (byte) Integer.parseInt(byteStrings[i].substring(2), 16); + } else { + // 잘못된 형식의 문자열인 경우 처리할 작업 + imageData[i] = 0; + } + } + + String fileName = UUID.randomUUID().toString(); + String fileUrl = ""; + + try { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType("image/jpeg"); + objectMetadata.setContentLength(imageData.length); + + InputStream inputStream = new ByteArrayInputStream(imageData); + amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + + fileUrl = amazonS3.getUrl(bucket, fileName).toString(); + } catch (Exception e) { + e.printStackTrace(); + } + + return fileUrl; + } + + public List uploadImages(List imageDataList) { + if (imageDataList.isEmpty()) + return null; + List fileUrlList = new ArrayList<>(); + + imageDataList.forEach(imageData -> { + String fileName = UUID.randomUUID().toString(); + String fileUrl = ""; + + try { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType("image/jpeg"); + objectMetadata.setContentLength(imageData.length); + + InputStream inputStream = new ByteArrayInputStream(imageData); + amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + + fileUrl = amazonS3.getUrl(bucket, fileName).toString(); + } catch (Exception e) { + log.error(e.getMessage()); + throw new InternalServerException(S3_UPLOAD_ERROR); + } + + fileUrlList.add(fileUrl); + }); + + return fileUrlList; + } + public void deleteImage(String fileName) { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } + public String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + try { + return fileName.substring(fileName.lastIndexOf(".")); + } catch(StringIndexOutOfBoundsException e) { + throw new InvalidValueException(INVALID_IMAGE_TYPE); + } + } + +} + diff --git a/socket/src/main/java/Backend/socket/infra/external/fcm/MessageTemplate.java b/socket/src/main/java/Backend/socket/infra/external/fcm/MessageTemplate.java new file mode 100644 index 0000000..fe66a75 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/external/fcm/MessageTemplate.java @@ -0,0 +1,20 @@ +package Backend.socket.infra.external.fcm; + +import lombok.Getter; + +@Getter +public enum MessageTemplate { + COMMENT("커뮤니티","\"%s\"글에 \"%s\"님이 댓글을 달았어요."), + CHAT("채팅","\"%s\"의 새로운 메세지가 도착했어요."), + CHAT_ROOM_NOTICE("공지", "\"%s\" 싱크의 새로운 채팅방이 생겼어요."), + SYNC_REMINDER("일정", "%s님! 오늘은 \"%s\" 싱크하는 날이에요."), + REVIEW("후기","%s님! 즐거운 싱크 되셨나요?"); + + private final String title; + private final String content; + + MessageTemplate(String title, String content) { + this.title = title; + this.content = content; + } +} diff --git a/socket/src/main/java/Backend/socket/infra/external/fcm/repository/FCMTokenRepository.java b/socket/src/main/java/Backend/socket/infra/external/fcm/repository/FCMTokenRepository.java new file mode 100644 index 0000000..321d106 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/external/fcm/repository/FCMTokenRepository.java @@ -0,0 +1,28 @@ +package Backend.socket.infra.external.fcm.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FCMTokenRepository { + private final StringRedisTemplate tokenRedisTemplate; + + public void saveToken(String userId, String fcmToken) { + tokenRedisTemplate.opsForValue() + .set(userId, fcmToken); + } + + public String getToken(String userId) { + return tokenRedisTemplate.opsForValue().get(userId); + } + + public void deleteToken(String userId) { + tokenRedisTemplate.delete(userId); + } + + public boolean hasKey(String userId) { + return tokenRedisTemplate.hasKey(userId); + } +} diff --git a/socket/src/main/java/Backend/socket/infra/external/fcm/service/PushNotificationService.java b/socket/src/main/java/Backend/socket/infra/external/fcm/service/PushNotificationService.java new file mode 100644 index 0000000..0b449a8 --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/external/fcm/service/PushNotificationService.java @@ -0,0 +1,134 @@ +package Backend.socket.infra.external.fcm.service; + +import Backend.socket.domain.chat.application.controller.dto.response.ChatMessageElementResponseDto; +import Backend.socket.domain.chat.domain.Room; +import Backend.socket.domain.chat.domain.User; +import Backend.socket.domain.chat.domain.notification.entity.NotificationHistory; +import Backend.socket.domain.chat.domain.notification.entity.NotificationType; +import Backend.socket.domain.chat.domain.notification.entity.TopCategory; +import Backend.socket.domain.chat.domain.notification.repository.NotificationHistoryRepository; +import Backend.socket.domain.chat.repository.RoomRepository; +import Backend.socket.domain.chat.repository.UserRepository; +import Backend.socket.infra.external.fcm.MessageTemplate; +import Backend.socket.infra.external.fcm.repository.FCMTokenRepository; +import Backend.socket.infra.external.fcm.service.dto.NotificationDto; +import com.google.firebase.messaging.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PushNotificationService { + private final FirebaseMessaging firebaseMessaging; + private final FCMTokenRepository fcmTokenRepository; + + private final UserRepository userRepository; + private final NotificationHistoryRepository notificationHistoryRepository; + + @Transactional + public void sendChatMessageNotification(Room room, ChatMessageElementResponseDto chatMessage, + List sessionIdList) { + for(String sessionId : sessionIdList) { + //단톡방에 있는 사람들 + User user = userRepository.findBySessionId(sessionId).orElseThrow(); + if(user.getSessionId().equals(chatMessage.getSessionId())) { //자기 자신 제외 알림. + continue; + } + User fromUser = userRepository.findBySessionId(chatMessage.getSessionId()).orElseThrow(); + + NotificationDto dto = NotificationDto.getChatMessageAlarm( + user.getId(), //받는이 + room.getRoomName(), //채팅방 이름 + fromUser.getId(), //보내는 이 + MessageTemplate.CHAT, //채팅 템플릿 + room.getRoomSession(), //방 세션 정보 + chatMessage.getContent() //채팅 내용 + ); + sendMessage(dto); + + //알림 기록 + LocalDateTime now = LocalDateTime.now(); + NotificationHistory history = NotificationHistory.createHistory( + user, + dto.getTemplate().getTitle(), + createMessageBody(dto), + getToken(dto.getId()), + now, + NotificationType.CHAT, + TopCategory.ACTIVITY, + room.getRoomName(), + "" + ); + notificationHistoryRepository.save(history); + + log.info("chatMessage notification sent to {} successfully. ", user.getUserName()); + } + + } + + private void sendMessage(NotificationDto dto) { + //FCM 토큰 확인 + if (!hasKey(dto.getId())) { + log.warn("FCM token not found for user with ID:" + dto.getId()); + return; + } + + //메세지 보내기 + try{ + firebaseMessaging.send(createMessage(dto)); + } catch (FirebaseMessagingException e) { + if(e.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) { + log.error("FCM token for user {} is invalid or unregistered", dto.getId()); + deleteToken(dto.getId()); + } else { + log.error("Failed to send FCM message to user {}", dto.getId()); + } + } + } + + private Message createMessage(NotificationDto dto) { + AndroidConfig androidConfig = AndroidConfig.builder() + .setNotification(AndroidNotification.builder() + .setChannelId(dto.getChannelId()) + .setTitle(dto.getTemplate().getTitle()) + .setBody(createMessageBody(dto)) + .build()) + .build(); + + return Message.builder() + .setToken(getToken(dto.getId())) + .setAndroidConfig(androidConfig) + .build(); + } + + private String createMessageBody(NotificationDto dto) { + if(dto.getStr2() != null && !dto.getStr2().isEmpty()) { + return String.format(dto.getTemplate().getContent(), dto.getStr1(), dto.getStr2()); + } + if(dto.getStr1() != null && !dto.getStr1().isEmpty()) { + return String.format(dto.getTemplate().getContent(), dto.getStr1()); + } + return dto.getTemplate().getContent(); + } + + public void saveToken(String id, String fcmToken) { fcmTokenRepository.saveToken(id, fcmToken); } + + public void deleteToken(String id) { + fcmTokenRepository.deleteToken(id); + } + + private String getToken(String id) { + return fcmTokenRepository.getToken(id); + } + + private boolean hasKey(String id) { + return fcmTokenRepository.hasKey(id); + } +} \ No newline at end of file diff --git a/socket/src/main/java/Backend/socket/infra/external/fcm/service/dto/NotificationDto.java b/socket/src/main/java/Backend/socket/infra/external/fcm/service/dto/NotificationDto.java new file mode 100644 index 0000000..0cde9ba --- /dev/null +++ b/socket/src/main/java/Backend/socket/infra/external/fcm/service/dto/NotificationDto.java @@ -0,0 +1,46 @@ +package Backend.socket.infra.external.fcm.service.dto; + +import Backend.socket.infra.external.fcm.MessageTemplate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class NotificationDto { + private String id; //알림 받는 이 + private String str1; //알림 내역1 + private String str2; //알림 내역2 + /** + * 커뮤니티 : 글이름 + 댓글단이 -> 글 Id, 댓글 Id + * 일정 : 유저이름 + 싱크이름 -> 싱크 Id + * 채팅방 개설 공지 : 싱크이름 -> 채팅방 Id + * 채팅 : 채팅내용 -> 채팅방 Id + * + * TODO + * 후기 : 유저이름 -> 마이페이지 + */ + private MessageTemplate template; + private String infoId; + private String infoId2; + private String channelId; + + public static NotificationDto getChatMessageAlarm(Long userId, String roomName, Long fromUserId, + MessageTemplate template, + String roomSessionId, + String content) { + return NotificationDto.builder() + .id(userId.toString()) + .str1(roomName) + .str2(fromUserId.toString()) + .template(template) + .infoId(roomSessionId) + .infoId2(content) + .channelId("ChatChannel") + .build(); + } + +} \ No newline at end of file diff --git a/socket/src/test/java/Backend/socket/SocketApplicationTests.java b/socket/src/test/java/Backend/socket/SocketApplicationTests.java new file mode 100644 index 0000000..3fa3e4d --- /dev/null +++ b/socket/src/test/java/Backend/socket/SocketApplicationTests.java @@ -0,0 +1,13 @@ +package Backend.socket; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SocketApplicationTests { + + @Test + void contextLoads() { + } + +}