diff --git a/.github/workflows/cd-workflow-dev.yml b/.github/workflows/cd-workflow-dev.yml index 4e26195bd..41969f200 100644 --- a/.github/workflows/cd-workflow-dev.yml +++ b/.github/workflows/cd-workflow-dev.yml @@ -11,6 +11,7 @@ permissions: env: RESOURCE_PATH: src/main/resources COMPOSE_PATH: /home/ec2-user/compose + FIREBASE_KEY_PATH: ${{ secrets.FIREBASE_KEY_PATH }} APP_PORT: 8000 jobs: @@ -33,6 +34,12 @@ jobs: echo "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml shell: bash + - name: πŸ” Create Firebase Key from secret (base64) + run: | + mkdir -p ${{ env.FIREBASE_KEY_PATH }} + echo "${{ secrets.FIREBASE_KEY }}" | base64 --decode > ${{ env.FIREBASE_KEY_PATH }}/serviceAccountKey.json + shell: bash + - name: 🐘 Cache Gradle dependencies uses: actions/cache@v3 with: diff --git a/.github/workflows/cd-workflow-prod.yml b/.github/workflows/cd-workflow-prod.yml index 651aecdfa..52117eb46 100644 --- a/.github/workflows/cd-workflow-prod.yml +++ b/.github/workflows/cd-workflow-prod.yml @@ -11,6 +11,7 @@ permissions: env: RESOURCE_PATH: src/main/resources COMPOSE_PATH: /home/ec2-user/compose + FIREBASE_KEY_PATH: ${{ secrets.FIREBASE_KEY_PATH }} APP_PORT: 8000 jobs: @@ -33,6 +34,12 @@ jobs: echo "${{ secrets.APPLICATION_YML_PROD }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml shell: bash + - name: πŸ” Create Firebase Key from secret (base64) + run: | + mkdir -p ${{ env.FIREBASE_KEY_PATH }} + echo "${{ secrets.FIREBASE_KEY }}" | base64 --decode > ${{ env.FIREBASE_KEY_PATH }}/serviceAccountKey.json + shell: bash + - name: 🐘 Cache Gradle dependencies uses: actions/cache@v3 with: diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 525ecbb7f..615cc801c 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -14,6 +14,7 @@ permissions: env: RESOURCE_PATH: src/main/resources TEST_RESOURCE_PATH: src/test/resources + FIREBASE_KEY_PATH: ${{ secrets.FIREBASE_KEY_PATH }} jobs: build: @@ -36,6 +37,12 @@ jobs: echo "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml echo "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml + - name: πŸ” Create Firebase Key from secret (base64) + run: | + mkdir -p ${{ env.FIREBASE_KEY_PATH }} + echo "${{ secrets.FIREBASE_KEY }}" | base64 --decode > ${{ env.FIREBASE_KEY_PATH }}/serviceAccountKey.json + shell: bash + - name: πŸ‘πŸ» grant execute permission for gradlew run: chmod +x gradlew diff --git a/.gitignore b/.gitignore index b10e200c4..e971d692c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/test/**/build/ *.yml deploy.sh +serviceAccountKey.json ### STS ### .apt_generated diff --git a/build.gradle b/build.gradle index 388a4ff77..74aa09e47 100644 --- a/build.gradle +++ b/build.gradle @@ -89,6 +89,8 @@ dependencies { implementation "org.flywaydb:flyway-core" implementation "org.flywaydb:flyway-mysql" + // Firebase + implementation 'com.google.firebase:firebase-admin:9.3.0' } def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile diff --git a/src/main/java/konkuk/thip/ThipServerApplication.java b/src/main/java/konkuk/thip/ThipServerApplication.java index 760455496..8a2d53b91 100644 --- a/src/main/java/konkuk/thip/ThipServerApplication.java +++ b/src/main/java/konkuk/thip/ThipServerApplication.java @@ -4,12 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @EnableJpaAuditing @EnableScheduling -@EnableAsync @ConfigurationPropertiesScan @SpringBootApplication public class ThipServerApplication { diff --git a/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java b/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java index e80b3999b..8e0f12a31 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java @@ -19,7 +19,7 @@ public class BookCleanUpService implements BookCleanUpUseCase { private final BookCommandPort bookCommandPort; private final BookQueryPort bookQueryPort; - @Async + @Async("schedulerAsyncExecutor") @Override @Transactional public void deleteUnusedBooks() { diff --git a/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java index 9711da9bf..50d90c1a5 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java @@ -28,7 +28,7 @@ public class BookMostSearchRankService { private final BookCommandPort bookCommandPort; // 맀일 0μ‹œ μ‹€ν–‰ - @Async + @Async("schedulerAsyncExecutor") @Scheduled(cron = "0 0 0 * * *") public void updateDailySearchRank() { diff --git a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java index 3858d06aa..3493e5d73 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -27,7 +27,7 @@ import static konkuk.thip.book.adapter.out.api.naver.NaverApiUtil.PAGE_SIZE; import static konkuk.thip.common.exception.code.ErrorCode.*; -import static konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType.BOOK_SEARCH; +import static konkuk.thip.recentSearch.domain.value.RecentSearchType.BOOK_SEARCH; @Service @RequiredArgsConstructor diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java index 0a61b2700..b959bbfb9 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java @@ -11,14 +11,20 @@ import konkuk.thip.comment.application.service.validator.CommentAuthorizationValidator; import konkuk.thip.comment.domain.Comment; import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; import konkuk.thip.post.domain.CountUpdatable; import konkuk.thip.post.application.service.handler.PostHandler; import konkuk.thip.post.domain.PostType; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import static konkuk.thip.common.exception.code.ErrorCode.INVALID_COMMENT_CREATE; +import static konkuk.thip.post.domain.PostType.*; @Service @@ -29,26 +35,31 @@ public class CommentCreateService implements CommentCreateUseCase { private final CommentQueryPort commentQueryPort; private final CommentLikeQueryPort commentLikeQueryPort; private final CommentQueryMapper commentQueryMapper; - + private final UserCommandPort userCommandPort; private final PostHandler postHandler; private final CommentAuthorizationValidator commentAuthorizationValidator; + private final FeedEventCommandPort feedEventCommandPort; + private final RoomEventCommandPort roomEventCommandPort; + @Override @Transactional public CommentCreateResponse createComment(CommentCreateCommand command) { // 1. λŒ“κΈ€/λ‹΅κΈ€ 생성 선행검증 및 μž‘μ„±ν•˜λ €λŠ” κ²Œμ‹œκΈ€ νƒ€μž… 검증 Comment.validateCommentCreate(command.isReplyRequest(), command.parentId()); - PostType type = PostType.from(command.postType()); + PostType type = from(command.postType()); // 2. κ²Œμ‹œλ¬Ό νƒ€μž…μ— 맞게 쑰회 CountUpdatable post = postHandler.findPost(type, command.postId()); // 2-1. κ²Œμ‹œκΈ€ νƒ€μž…μ— λ”°λ₯Έ λŒ“κΈ€ 생성 κΆŒν•œ 검증 commentAuthorizationValidator.validateUserCanAccessPostForComment(type, post, command.userId()); - // TODO ν”Όλ“œ: λ‚΄ κ²Œμ‹œκΈ€μ˜ λŒ“κΈ€, λ‚΄ λŒ“κΈ€μ— λŒ€ν•œ λ‹΅κΈ€ μ•Œλ¦Ό 전솑 - // TODO 기둝 및 νˆ¬ν‘œ: λͺ¨μž„λ°©μ˜ λ‚΄ κ²Œμ‹œκΈ€μ— λŒ€ν•œ λŒ“κΈ€, λ‚΄ λŒ“κΈ€μ— λŒ€ν•œ λ‹΅κΈ€ μ•Œλ¦Ό 전솑 + // 2-2. λŒ“κΈ€ 생성 푸쉬 μ•Œλ¦Ό 전솑 (κ²Œμ‹œκΈ€ μž‘μ„±μžμ—κ²Œ) + PostQueryDto postQueryDto = postHandler.getPostQueryDto(type, post.getId()); + User actorUser = userCommandPort.findById(command.userId()); + sendNotificationsToPostWriter(postQueryDto, actorUser); // 3. λŒ“κΈ€ 생성 Long savedCommentId = createCommentDomain(command); @@ -65,6 +76,10 @@ public CommentCreateResponse createComment(CommentCreateCommand command) { if (command.isReplyRequest()) { // λΆ€λͺ¨ λŒ“κΈ€ 쑰회 CommentQueryDto parentCommentDto = commentQueryPort.findRootCommentById(command.parentId()); + + // λ‹΅κΈ€ 생성 푸쉬 μ•Œλ¦Ό 전솑 (λΆ€λͺ¨ λŒ“κΈ€ μž‘μ„±μžμ—κ²Œ) + sendNotificationsToParentCommentWriter(postQueryDto, parentCommentDto, actorUser); + // μ‚¬μš©μž λΆ€λͺ¨ λŒ“κΈ€ μ’‹μ•„μš” μ—¬λΆ€ 쑰회 boolean isLikedParentComment = commentLikeQueryPort.isLikedCommentByUser(command.userId(),parentCommentDto.commentId()); @@ -74,7 +89,30 @@ public CommentCreateResponse createComment(CommentCreateCommand command) { CommentQueryDto savedCommentDto = commentQueryPort.findRootCommentById(savedCommentId); return commentQueryMapper.toRoot(savedCommentDto, false, command.userId()); } + } + + private void sendNotificationsToPostWriter(PostQueryDto postQueryDto, User actorUser) { + if (postQueryDto.creatorId().equals(actorUser.getId())) return; // μžμ‹ μ΄ μž‘μ„±ν•œ κ²Œμ‹œκΈ€ μ œμ™Έ + if (postQueryDto.postType().equals(FEED.getType())) { + // ν”Όλ“œ λŒ“κΈ€ μ•Œλ¦Ό 이벀트 λ°œν–‰ + feedEventCommandPort.publishFeedCommentedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); + } else if (postQueryDto.postType().equals(RECORD.getType()) || postQueryDto.postType().equals(VOTE.getType())) { + // λͺ¨μž„λ°© κ²Œμ‹œκΈ€ λŒ“κΈ€ μ•Œλ¦Ό 이벀트 λ°œν–‰ + roomEventCommandPort.publishRoomPostCommentedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + } + } + + private void sendNotificationsToParentCommentWriter(PostQueryDto postQueryDto, CommentQueryDto parentCommentDto, User actorUser) { + if (parentCommentDto.creatorId().equals(actorUser.getId())) return; // μžμ‹ μ΄ μž‘μ„±ν•œ λŒ“κΈ€ μ œμ™Έ + + if (postQueryDto.postType().equals(FEED.getType())) { + // ν”Όλ“œ λ‹΅κΈ€ μ•Œλ¦Ό 이벀트 λ°œν–‰ + feedEventCommandPort.publishFeedRepliedEvent(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); + } else if (postQueryDto.postType().equals(RECORD.getType()) || postQueryDto.postType().equals(VOTE.getType())) { + // λͺ¨μž„λ°© κ²Œμ‹œκΈ€ λ‹΅κΈ€ μ•Œλ¦Ό 이벀트 λ°œν–‰ + roomEventCommandPort.publishRoomPostCommentRepliedEvent(parentCommentDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + } } private Long createCommentDomain(CommentCreateCommand command) { diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java index fa4aa06c9..4fc0c4725 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentLikeService.java @@ -8,8 +8,14 @@ import konkuk.thip.comment.application.port.out.CommentLikeQueryPort; import konkuk.thip.comment.application.service.validator.CommentAuthorizationValidator; import konkuk.thip.comment.domain.Comment; -import konkuk.thip.post.domain.CountUpdatable; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; import konkuk.thip.post.application.service.handler.PostHandler; +import konkuk.thip.post.domain.CountUpdatable; +import konkuk.thip.post.domain.PostType; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,10 +27,14 @@ public class CommentLikeService implements CommentLikeUseCase { private final CommentCommandPort commentCommandPort; private final CommentLikeQueryPort commentLikeQueryPort; private final CommentLikeCommandPort commentLikeCommandPort; + private final UserCommandPort userCommandPort; private final PostHandler postHandler; private final CommentAuthorizationValidator commentAuthorizationValidator; + private final FeedEventCommandPort feedEventCommandPort; + private final RoomEventCommandPort roomEventCommandPort; + @Override @Transactional public CommentIsLikeResult changeLikeStatusComment(CommentIsLikeCommand command) { @@ -42,6 +52,9 @@ public CommentIsLikeResult changeLikeStatusComment(CommentIsLikeCommand command) if (command.isLike()) { comment.validateCanLike(alreadyLiked); // μ’‹μ•„μš” κ°€λŠ₯ μ—¬λΆ€ 검증 commentLikeCommandPort.save(command.userId(), command.commentId()); + + // λŒ“κΈ€ μ’‹μ•„μš” ν‘Έμ‰¬μ•Œλ¦Ό 전솑 + sendNotifications(command, comment); } else { comment.validateCanUnlike(alreadyLiked); // μ’‹μ•„μš” μ·¨μ†Œ κ°€λŠ₯ μ—¬λΆ€ 검증 commentLikeCommandPort.delete(command.userId(), command.commentId()); @@ -53,4 +66,18 @@ public CommentIsLikeResult changeLikeStatusComment(CommentIsLikeCommand command) return CommentIsLikeResult.of(comment.getId(), command.isLike()); } + + private void sendNotifications(CommentIsLikeCommand command, Comment comment) { + if (command.userId().equals(comment.getCreatorId())) return; // μžμ‹ μ˜ λŒ“κΈ€μ— μ’‹μ•„μš” λˆ„λ₯΄λŠ” 경우 μ œμ™Έ + + User actorUser = userCommandPort.findById(command.userId()); + // μ’‹μ•„μš” ν‘Έμ‰¬μ•Œλ¦Ό 전솑 + if (comment.getPostType() == PostType.FEED) { + feedEventCommandPort.publishFeedCommentLikedEvent(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), comment.getTargetPostId()); + } + if (comment.getPostType() == PostType.RECORD || comment.getPostType() == PostType.VOTE) { + PostQueryDto postQueryDto = postHandler.getPostQueryDto(comment.getPostType(), comment.getTargetPostId()); + roomEventCommandPort.publishRoomCommentLikedEvent(comment.getCreatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + } + } } diff --git a/src/main/java/konkuk/thip/common/exception/FirebaseException.java b/src/main/java/konkuk/thip/common/exception/FirebaseException.java new file mode 100644 index 000000000..a4d97a046 --- /dev/null +++ b/src/main/java/konkuk/thip/common/exception/FirebaseException.java @@ -0,0 +1,21 @@ +package konkuk.thip.common.exception; + +import konkuk.thip.common.exception.code.ErrorCode; + +public class FirebaseException extends BusinessException { + public FirebaseException(ErrorCode errorCode) { + super(errorCode); + } + + public FirebaseException(ErrorCode errorCode, Exception e) { + super(errorCode, e); + } + + public FirebaseException(Exception e) { + super(ErrorCode.FIREBASE_SEND_ERROR, e); + } + + public FirebaseException() { + super(ErrorCode.FIREBASE_SEND_ERROR); + } +} diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 98c8308f3..f944caa6b 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -215,6 +215,13 @@ public enum ErrorCode implements ResponseCode { ATTENDANCE_CHECK_NOT_FOUND(HttpStatus.NOT_FOUND, 195001, "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ATTENDANCE CHECK μž…λ‹ˆλ‹€."), ATTENDANCE_CHECK_CAN_NOT_DELETE(HttpStatus.FORBIDDEN, 195002, "였늘의 ν•œλ§ˆλ””λŠ” 본인만 μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€."), + /** + * 200000 : Fcm error + */ + FCM_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, 200000, "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” FCM TOKEN μž…λ‹ˆλ‹€."), + FCM_TOKEN_ENABLED_STATE_ALREADY(HttpStatus.BAD_REQUEST, 200001, "μš”μ²­ν•œ μƒνƒœλ‘œ 이미 푸쉬 μ•Œλ¦Ό μ—¬λΆ€κ°€ μ„€μ •λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€."), + FCM_TOKEN_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 200002, "토큰을 μ†Œμœ ν•˜κ³  μžˆλŠ” 계정이 μ•„λ‹™λ‹ˆλ‹€."), + FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 μ•Œλ¦Ό 전솑에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.") ; diff --git a/src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java b/src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java new file mode 100644 index 000000000..ed71adeb8 --- /dev/null +++ b/src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java @@ -0,0 +1,24 @@ +package konkuk.thip.common.scheduler; + +import konkuk.thip.room.application.port.in.RoomStateChangeUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RoomStateScheduler { + + private final RoomStateChangeUseCase roomStateChangeUseCase; + + // 맀일 μžμ • μ‹€ν–‰ + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + public void changeRoomState() { + log.info("[μŠ€μΌ€μ€„λŸ¬] λ°© μƒνƒœ λ³€κ²½ μ‹œμž‘"); + roomStateChangeUseCase.changeRoomStateToExpired(); + roomStateChangeUseCase.changeRoomStateToProgress(); + log.info("[μŠ€μΌ€μ€„λŸ¬] λ°© μƒνƒœ λ³€κ²½ μ™„λ£Œ"); + } +} diff --git a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java index 8be9688a2..df2073145 100644 --- a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java +++ b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java @@ -1,5 +1,10 @@ package konkuk.thip.common.security.util; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.SignatureException; import io.jsonwebtoken.*; import konkuk.thip.common.security.oauth2.LoginUser; import lombok.extern.slf4j.Slf4j; @@ -50,6 +55,8 @@ public boolean validateToken(String token) { try { Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); return true; + } catch (SignatureException e) { + log.info("Invalid JWT Signature", e); } catch (MalformedJwtException e) { log.info("Invalid JWT Token", e); } catch (ExpiredJwtException e) { diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 3482586cc..703e7f4dc 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -334,6 +334,22 @@ public enum SwaggerResponseDescription { ATTENDANCE_CHECK_CAN_NOT_DELETE ))), + // Notiification + FCM_TOKEN_REGISTER(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + FCM_TOKEN_NOT_FOUND + ))), + FCM_TOKEN_ENABLE_STATE_CHANGE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + FCM_TOKEN_NOT_FOUND, + FCM_TOKEN_ENABLED_STATE_ALREADY, + FCM_TOKEN_ACCESS_FORBIDDEN + ))), + FCM_TOKEN_DELETE(new LinkedHashSet<>(Set.of( + FCM_TOKEN_NOT_FOUND, + FCM_TOKEN_ACCESS_FORBIDDEN + ))) + ; private final Set errorCodeList; SwaggerResponseDescription(Set errorCodeList) { diff --git a/src/main/java/konkuk/thip/config/FirebaseConfig.java b/src/main/java/konkuk/thip/config/FirebaseConfig.java new file mode 100644 index 000000000..82037dce7 --- /dev/null +++ b/src/main/java/konkuk/thip/config/FirebaseConfig.java @@ -0,0 +1,46 @@ +package konkuk.thip.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; + +@Configuration +public class FirebaseConfig { + + @Bean + FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) { + return FirebaseMessaging.getInstance(firebaseApp); + } + + @Bean + FirebaseApp firebaseApp(@Value("${firebase.file-path}") String filePath) throws IOException { + GoogleCredentials googleCredentials; + ClassPathResource resource = new ClassPathResource(filePath); + try (InputStream in = resource.getInputStream()) { + googleCredentials = GoogleCredentials.fromStream(in); + } + + FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setCredentials(googleCredentials) + .build(); + + if (FirebaseApp.getApps().isEmpty()) { + return FirebaseApp.initializeApp(firebaseOptions); + } + return FirebaseApp.getInstance(); + } + + @Bean + FirebaseAuth firebaseAuth(FirebaseApp firebaseApp) { + return FirebaseAuth.getInstance(firebaseApp); + } +} diff --git a/src/main/java/konkuk/thip/config/WorkerThreadConfig.java b/src/main/java/konkuk/thip/config/WorkerThreadConfig.java new file mode 100644 index 000000000..0f32626ef --- /dev/null +++ b/src/main/java/konkuk/thip/config/WorkerThreadConfig.java @@ -0,0 +1,66 @@ +package konkuk.thip.config; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration +@EnableAsync +@Profile("!test") +public class WorkerThreadConfig implements AsyncConfigurer { + + /** + * FCM ν‘Έμ‹œμ•Œλ¦Ό μ „μš© μ‹€ν–‰κΈ° + * - 이벀트 λ¦¬μŠ€λ„ˆλ“€μ˜ μ•Œλ¦Ό λ°œμ†‘ 처리 + * - λΉˆλ²ˆν•œ 짧은 I/O μž‘μ—… μ΅œμ ν™” + */ + @Bean(name = "fcmAsyncExecutor") + public Executor fcmAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(6); // FCM μ•Œλ¦Ό κΈ°λ³Έ μ²˜λ¦¬λŸ‰ + executor.setMaxPoolSize(15); // μ•Œλ¦Ό 급증 μ‹œ ν™•μž₯ + executor.setQueueCapacity(100); // μ•Œλ¦Ό λŒ€κΈ° 큐 (μ λ‹Ήν•œ 크기) + executor.setThreadNamePrefix("fcm-"); + executor.setWaitForTasksToCompleteOnShutdown(true); +// executor.setAwaitTerminationSeconds(30); + executor.initialize(); + return executor; + } + + /** + * μŠ€μΌ€μ€„λŸ¬ λ°°μΉ˜μž‘μ—… μ „μš© μ‹€ν–‰κΈ° + * - 데이터 정리, λ°© μƒνƒœ λ³€κ²½ λ“± 배치 μž‘μ—… + * - κΈ΄ μ‹€ν–‰μ‹œκ°„ μž‘μ—… μ΅œμ ν™” + */ + @Bean(name = "schedulerAsyncExecutor") + public Executor schedulerAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); // λ°°μΉ˜μž‘μ—…μ€ 적은 λ™μ‹œ μ‹€ν–‰ + executor.setMaxPoolSize(4); // μ΅œλŒ€ ν™•μž₯도 μ œν•œμ  + executor.setQueueCapacity(10); // μž‘μ€ 큐 (μŠ€μΌ€μ€„λ§λœ μž‘μ—…μ΄λ―€λ‘œ) + executor.setThreadNamePrefix("scheduler-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(120); // λ°°μΉ˜μž‘μ—… μ™„λ£Œ λŒ€κΈ°μ‹œκ°„ + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + + @Override + public Executor getAsyncExecutor() { + return fcmAsyncExecutor(); // 기본은 FCM ν’€ μ‚¬μš© + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java index 5b9e3e7e6..58228d1ec 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java @@ -5,13 +5,14 @@ import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; -import konkuk.thip.feed.adapter.out.jpa.*; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.feed.adapter.out.mapper.FeedMapper; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.feed.application.port.out.FeedCommandPort; import konkuk.thip.feed.domain.Feed; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java index e6321fb01..a9b2fcfdc 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java @@ -1,6 +1,5 @@ package konkuk.thip.feed.adapter.out.persistence.repository; -import konkuk.thip.feed.application.port.out.dto.TagCategoryQueryDto; import konkuk.thip.feed.application.port.out.dto.FeedQueryDto; import java.time.LocalDateTime; diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java index 4951ecb2e..fbd079a4a 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java @@ -13,6 +13,8 @@ import konkuk.thip.feed.adapter.out.jpa.QSavedFeedJpaEntity; import konkuk.thip.feed.application.port.out.dto.FeedQueryDto; import konkuk.thip.feed.application.port.out.dto.QFeedQueryDto; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; +import konkuk.thip.post.application.port.out.dto.QPostQueryDto; import konkuk.thip.user.adapter.out.jpa.QFollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java index 355e1ba66..a1aea4ac4 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java @@ -9,13 +9,19 @@ import konkuk.thip.feed.application.port.in.dto.FeedCreateCommand; import konkuk.thip.feed.application.port.out.FeedCommandPort; import konkuk.thip.feed.domain.Feed; +import konkuk.thip.feed.domain.value.ContentList; import konkuk.thip.feed.domain.value.Tag; import konkuk.thip.feed.domain.value.TagList; -import konkuk.thip.feed.domain.value.ContentList; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.application.port.out.UserQueryPort; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor public class FeedCreateService implements FeedCreateUseCase { @@ -23,8 +29,11 @@ public class FeedCreateService implements FeedCreateUseCase { private final BookCommandPort bookCommandPort; private final FeedCommandPort feedCommandPort; private final BookApiQueryPort bookApiQueryPort; + private final UserQueryPort userQueryPort; + private final UserCommandPort userCommandPort; private final ImageUrlValidationService imageUrlValidationService; + private final FeedEventCommandPort feedEventCommandPort; @Override @Transactional @@ -48,7 +57,22 @@ public Long createFeed(FeedCreateCommand command) { command.tagList(), command.imageUrls() ); - return feedCommandPort.save(feed); + + // 4. ν”Όλ“œ μ˜μ†ν™” + Long savedFeedId = feedCommandPort.save(feed); + + // 5. ν”Όλ“œ μž‘μ„± 푸쉬 μ•Œλ¦Ό 전솑 + sendNotifications(command, savedFeedId); + + return savedFeedId; + } + + private void sendNotifications(FeedCreateCommand command, Long savedFeedId) { + List targetUsers = userQueryPort.getAllFollowersByUserId(command.userId()); + User actorUser = userCommandPort.findById(command.userId()); + for (User targetUser : targetUsers) { + feedEventCommandPort.publishFolloweeNewPostEvent(targetUser.getId(), actorUser.getId(), actorUser.getNickname(), savedFeedId); + } } /** diff --git a/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java b/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java new file mode 100644 index 000000000..e8e97855e --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListener.java @@ -0,0 +1,52 @@ +package konkuk.thip.message.adapter.in.event; + +import konkuk.thip.message.adapter.out.event.dto.FeedEvents; +import konkuk.thip.message.application.port.in.FeedNotificationDispatchUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class MessageFeedEventListener { + + private final FeedNotificationDispatchUseCase feedUseCase; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onFollower(FeedEvents.FollowerEvent e) { + feedUseCase.handleFollower(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onFeedCommented(FeedEvents.FeedCommentedEvent e) { + feedUseCase.handleFeedCommented(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onFeedCommentReplied(FeedEvents.FeedCommentRepliedEvent e) { + feedUseCase.handleFeedCommentReplied(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onFolloweeNewPost(FeedEvents.FolloweeNewPostEvent e) { + feedUseCase.handleFolloweeNewPost(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onFeedLiked(FeedEvents.FeedLikedEvent e) { + feedUseCase.handleFeedLiked(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onFeedCommentLiked(FeedEvents.FeedCommentLikedEvent e) { + feedUseCase.handleFeedCommentLiked(e); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListener.java b/src/main/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListener.java new file mode 100644 index 000000000..7582dbaa3 --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListener.java @@ -0,0 +1,70 @@ +package konkuk.thip.message.adapter.in.event; + +import konkuk.thip.message.adapter.out.event.dto.RoomEvents; +import konkuk.thip.message.application.port.in.RoomNotificationDispatchUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class MessageRoomEventListener { + + private final RoomNotificationDispatchUseCase roomUseCase; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomPostCommented(RoomEvents.RoomPostCommentedEvent e) { + roomUseCase.handleRoomPostCommented(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomVoteStarted(RoomEvents.RoomVoteStartedEvent e) { + roomUseCase.handleRoomVoteStarted(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomRecordCreated(RoomEvents.RoomRecordCreatedEvent e) { + roomUseCase.handleRoomRecordCreated(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomRecruitClosedEarly(RoomEvents.RoomRecruitClosedEarlyEvent e) { + roomUseCase.handleRoomRecruitClosedEarly(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomActivityStarted(RoomEvents.RoomActivityStartedEvent e) { + roomUseCase.handleRoomActivityStarted(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomJoinRequestedToOwner(RoomEvents.RoomJoinRequestedToOwnerEvent e) { + roomUseCase.handleRoomJoinRequestedToOwner(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomCommentLiked(RoomEvents.RoomCommentLikedEvent e) { + roomUseCase.handleRoomCommentLiked(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomPostLiked(RoomEvents.RoomPostLikedEvent e) { + roomUseCase.handleRoomPostLiked(e); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent e) { + roomUseCase.handleRoomPostCommentReplied(e); + } +} diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java new file mode 100644 index 000000000..b826b181e --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/out/event/FeedEventPublisherAdapter.java @@ -0,0 +1,78 @@ +package konkuk.thip.message.adapter.out.event; + +import konkuk.thip.message.adapter.out.event.dto.FeedEvents; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class FeedEventPublisherAdapter implements FeedEventCommandPort { + + private final ApplicationEventPublisher publisher; + + @Override + public void publishFollowEvent(Long targetUserId, Long actorUserId, String actorUsername) { + publisher.publishEvent(FeedEvents.FollowerEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .build()); + } + + @Override + public void publishFeedCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { + publisher.publishEvent(FeedEvents.FeedCommentedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .feedId(feedId) + .build()); + } + + @Override + public void publishFeedRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { + publisher.publishEvent(FeedEvents.FeedCommentRepliedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .feedId(feedId) + .build()); + } + + @Override + public void publishFolloweeNewPostEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { + publisher.publishEvent(FeedEvents.FolloweeNewPostEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .feedId(feedId) + .build()); + } + + @Override + public void publishFeedLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { + publisher.publishEvent(FeedEvents.FeedLikedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .feedId(feedId) + .build()); + } + + @Override + public void publishFeedCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) { + publisher.publishEvent(FeedEvents.FeedCommentLikedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .feedId(feedId) + .build()); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java new file mode 100644 index 000000000..c8cf0c5d7 --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/out/event/RoomEventPublisherAdapter.java @@ -0,0 +1,125 @@ +package konkuk.thip.message.adapter.out.event; + +import konkuk.thip.message.adapter.out.event.dto.RoomEvents; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RoomEventPublisherAdapter implements RoomEventCommandPort { + + private final ApplicationEventPublisher publisher; + + @Override + public void publishRoomPostCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + publisher.publishEvent(RoomEvents.RoomPostCommentedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .roomId(roomId) + .page(page) + .postId(postId) + .postType(postType) + .build()); + } + + @Override + public void publishRoomVoteStartedEvent(Long targetUserId, Long roomId, String roomTitle, + Integer page, Long postId) { + publisher.publishEvent(RoomEvents.RoomVoteStartedEvent.builder() + .targetUserId(targetUserId) + .roomId(roomId) + .roomTitle(roomTitle) + .page(page) + .postId(postId) + .build()); + } + + @Override + public void publishRoomRecordCreatedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId) { + publisher.publishEvent(RoomEvents.RoomRecordCreatedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .roomId(roomId) + .roomTitle(roomTitle) + .page(page) + .postId(postId) + .build()); + } + + @Override + public void publishRoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, String roomTitle) { + publisher.publishEvent(RoomEvents.RoomRecruitClosedEarlyEvent.builder() + .targetUserId(targetUserId) + .roomId(roomId) + .roomTitle(roomTitle) + .build()); + } + + @Override + public void publishRoomActivityStartedEvent(Long targetUserId, Long roomId, String roomTitle) { + publisher.publishEvent(RoomEvents.RoomActivityStartedEvent.builder() + .targetUserId(targetUserId) + .roomId(roomId) + .roomTitle(roomTitle) + .build()); + } + + @Override + public void publishRoomJoinEventToHost(Long hostUserId, Long roomId, String roomTitle, + Long actorUserId, String actorUsername) { + publisher.publishEvent(RoomEvents.RoomJoinRequestedToOwnerEvent.builder() + .ownerUserId(hostUserId) + .roomId(roomId) + .roomTitle(roomTitle) + .applicantUserId(actorUserId) + .applicantUsername(actorUsername) + .build()); + } + + @Override + public void publishRoomCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + publisher.publishEvent(RoomEvents.RoomCommentLikedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .roomId(roomId) + .page(page) + .postId(postId) + .postType(postType) + .build()); + } + + @Override + public void publishRoomPostLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) { + publisher.publishEvent(RoomEvents.RoomPostLikedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .roomId(roomId) + .page(page) + .postId(postId) + .postType(postType) + .build()); + } + + @Override + public void publishRoomPostCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, Long roomId, Integer page, Long postId, String postType) { + publisher.publishEvent(RoomEvents.RoomPostCommentRepliedEvent.builder() + .targetUserId(targetUserId) + .actorUserId(actorUserId) + .actorUsername(actorUsername) + .roomId(roomId) + .page(page) + .postId(postId) + .postType(postType) + .build()); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java b/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java new file mode 100644 index 000000000..176f1f801 --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/out/event/dto/FeedEvents.java @@ -0,0 +1,36 @@ +// message/adapter/out/event/dto/FeedEvents.java +package konkuk.thip.message.adapter.out.event.dto; + +import lombok.Builder; + +public class FeedEvents { + + // λˆ„κ΅°κ°€ λ‚˜λ₯Ό νŒ”λ‘œμš°ν•˜λŠ” 경우 + @Builder + public record FollowerEvent(Long targetUserId, Long actorUserId, String actorUsername) {} + + // λˆ„κ΅°κ°€ λ‚΄ ν”Όλ“œμ— λŒ“κΈ€μ„ λ‹€λŠ” 경우 + @Builder + public record FeedCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} + + // λˆ„κ΅°κ°€ λ‚΄ λŒ“κΈ€μ— λŒ€λŒ“κΈ€μ„ λ‹€λŠ” 경우 + @Builder + public record FeedCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} + + // λ‚΄κ°€ νŒ”λ‘œμš°ν•˜λŠ” μ‚¬λžŒμ΄ μƒˆ 글을 μ˜¬λ¦¬λŠ” 경우 + @Builder + public record FolloweeNewPostEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} + + // λ‚΄ ν”Όλ“œκ°€ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + @Builder + public record FeedLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} + + // λ‚΄ ν”Όλ“œ λŒ“κΈ€μ΄ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + @Builder + public record FeedCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId) {} +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java b/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java new file mode 100644 index 000000000..bdc076f93 --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/out/event/dto/RoomEvents.java @@ -0,0 +1,51 @@ +// message/adapter/out/event/dto/RoomEvents.java +package konkuk.thip.message.adapter.out.event.dto; + +import lombok.Builder; + +public class RoomEvents { + + // λŒ“κΈ€ λŒ€μƒμ΄ "기둝/νˆ¬ν‘œ" λͺ¨λ‘ κ°€λŠ₯ν•˜λ―€λ‘œ 톡합 μŠ€ν‚€λ§ˆ μ‚¬μš© + // λ‚΄ λͺ¨μž„λ°© 기둝/νˆ¬ν‘œμ— λŒ“κΈ€μ΄ 달린 경우 + @Builder + public record RoomPostCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„방에 μƒˆλ‘œμš΄ νˆ¬ν‘œκ°€ μ‹œμž‘λœ 경우 + @Builder + public record RoomVoteStartedEvent(Long targetUserId, Long roomId, String roomTitle, + Integer page, Long postId) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„방에 μƒˆλ‘œμš΄ 기둝이 μž‘μ„±λœ 경우 + @Builder + public record RoomRecordCreatedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„방이 μ‘°κΈ° μ’…λ£Œλœ 경우 (ν˜ΈμŠ€νŠΈκ°€ λͺ¨μ§‘ 마감 λ²„νŠΌ λˆ„λ₯Έ 경우) + @Builder + public record RoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, String roomTitle) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°© ν™œλ™μ΄ μ‹œμž‘λœ 경우 (방이 μ‹œμž‘ 기간이 λ˜μ–΄ μžλ™μœΌλ‘œ μ‹œμž‘λœ 경우) + @Builder + public record RoomActivityStartedEvent(Long targetUserId, Long roomId, String roomTitle) {} + + // λ‚΄κ°€ λ°©μž₯일 λ•Œ, μƒˆλ‘œμš΄ μ‚¬μš©μžκ°€ λͺ¨μž„λ°© μ°Έμ—¬λ₯Ό ν•œ 경우 + @Builder + public record RoomJoinRequestedToOwnerEvent(Long ownerUserId, Long roomId, String roomTitle, + Long applicantUserId, String applicantUsername) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°©μ˜ λ‚˜μ˜ λŒ“κΈ€μ΄ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + @Builder + public record RoomCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°©μ˜ λ‚˜μ˜ 기둝이 μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + @Builder + public record RoomPostLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°©μ˜ λ‚˜μ˜ λŒ“κΈ€μ— λŒ€λŒ“κΈ€μ΄ 달린 경우 + @Builder + public record RoomPostCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType) {} +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/adapter/out/firebase/FakeFirebaseAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/firebase/FakeFirebaseAdapter.java new file mode 100644 index 000000000..0fe577bbd --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/out/firebase/FakeFirebaseAdapter.java @@ -0,0 +1,27 @@ +package konkuk.thip.message.adapter.out.firebase; + +import com.google.firebase.messaging.Message; +import konkuk.thip.message.application.port.out.FirebaseMessagingPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@Profile("local | test") +@RequiredArgsConstructor +public class FakeFirebaseAdapter implements FirebaseMessagingPort { + + @Override + public void send(Message message, String fcmToken, String deviceId) { + log.info("[FakeFCM:SEND] token={} device={} message={}", fcmToken, deviceId, message); + } + + @Override + public void sendBatch(List messages, List fcmTokens, List deviceIds) { + log.info("[FakeFCM:BATCH] count={} tokens={} devices={}", messages.size(), fcmTokens, deviceIds); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java new file mode 100644 index 000000000..1d7f64a83 --- /dev/null +++ b/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java @@ -0,0 +1,106 @@ +package konkuk.thip.message.adapter.out.firebase; + +import com.google.firebase.messaging.*; +import konkuk.thip.common.exception.FirebaseException; +import konkuk.thip.message.application.port.out.FirebaseMessagingPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@Profile("prod | dev") +@RequiredArgsConstructor +public class FirebaseAdapter implements FirebaseMessagingPort { + + private final FirebaseMessaging firebaseMessaging; + + @Value("${server.profile}") + private String profile; + + @Override + public void send(Message message, String fcmToken, String deviceId) { + try { + String messageId = firebaseMessaging.send(message); + log.debug("[FCM:SEND] ok id={} token={} device={}", messageId, maskDependingProfile(fcmToken), maskDependingProfile(deviceId)); + } catch (FirebaseMessagingException e) { + log.warn("[FCM:SEND] fail token={} device={} code={} msg={}", maskDependingProfile(fcmToken), maskDependingProfile(deviceId), e.getMessagingErrorCode(), e.getMessage()); + throw new FirebaseException(e); + } + } + + @Override + public void sendBatch(List messages, List fcmTokens, List deviceIds) { + if (messages.size() != fcmTokens.size() || messages.size() != deviceIds.size()) { + throw new FirebaseException(new IllegalArgumentException("λ©”μ‹œμ§€, FCM 토큰, λ””λ°”μ΄μŠ€ ID 리슀트의 ν¬κΈ°λŠ” κ°™μ•„μ•Ό ν•©λ‹ˆλ‹€.")); + } + + try { + BatchResponse batchResponse = firebaseMessaging.sendEach(messages); + + List responses = batchResponse.getResponses(); + for (int i = 0; i < responses.size(); i++) { + SendResponse sr = responses.get(i); + String token = fcmTokens.get(i); + String device = deviceIds.get(i); + + if (sr.isSuccessful()) { + log.debug("[FCM:BATCH] ok id={} token={} device={}", sr.getMessageId(), maskDependingProfile(token), maskDependingProfile(device)); + } else { + Exception ex = sr.getException(); + if (ex instanceof FirebaseMessagingException fme) { + log.warn("[FCM:BATCH] fail token={} device={} code={} msg={}", maskDependingProfile(token), maskDependingProfile(device), fme.getMessagingErrorCode(), fme.getMessage()); + } else { + log.warn("[FCM:BATCH] fail token={} device={} msg={}", maskDependingProfile(token), maskDependingProfile(device), ex.getMessage()); + } + } + } + + if (batchResponse.getFailureCount() > 0) { + log.warn("[FCM:BATCH] 일뢀 λ©”μ‹œμ§€ 전솑 μ‹€νŒ¨: {}/{}", batchResponse.getFailureCount(), messages.size()); + throw new FirebaseException(); + } + } catch (FirebaseMessagingException e) { + log.warn("[FCM:BATCH] λ©”μ‹œμ§€ 전솑 μ‹€νŒ¨: code={} msg={}", e.getMessagingErrorCode(), e.getMessage()); + throw new FirebaseException(e); + } + } + + /** + * ν”„λ‘œνŒŒμΌλ³„ λ§ˆμŠ€ν‚Ή μ •μ±… + * - dev: 원문 κ·ΈλŒ€λ‘œ λ…ΈμΆœ + * - prod: 전체 길이의 μ ˆλ°˜μ„ '*'둜 μΉ˜ν™˜(μΉ˜ν™˜λœ 개수만큼 λ³„ν‘œκ°€ 보이도둝), λ‚˜λ¨Έμ§€λŠ” μ•ž/λ’€λ₯Ό κ· λ“±ν•˜κ²Œ λ…ΈμΆœ + */ + private String maskDependingProfile(String value) { + if (isDev()) return value; + return maskHalf(value); + } + + private boolean isDev() { + return profile != null && profile.trim().equalsIgnoreCase("dev"); + } + + /** + * 전체 길이의 μ ˆλ°˜μ„ '*'둜 μΉ˜ν™˜ν•˜κ³ , 남은 μ ˆλ°˜μ€ μ•ž/λ’€λ₯Ό κ· λ“± λΆ„ν• ν•΄ λ…ΈμΆœ + * 예) abcdefghij(10) -> ab*****hij (μ•ž 2, 쀑간 5*, λ’€ 3) + */ + private String maskHalf(String s) { + if (s == null || s.isEmpty()) return "null"; + int len = s.length(); + if (len <= 4) return "*".repeat(len); // λ„ˆλ¬΄ 짧으면 μ „λΆ€ λ§ˆμŠ€ν‚Ή + + int maskLen = len / 2; // 절반 λ§ˆμŠ€ν‚Ή + int visible = len - maskLen; // λ³΄μ΄λŠ” 길이 + int left = visible / 2; // μ•žμͺ½ λ³΄μ΄λŠ” 길이 + int right = visible - left; // λ’€μͺ½ λ³΄μ΄λŠ” 길이 + + String prefix = s.substring(0, left); + String stars = "*".repeat(maskLen); + String suffix = s.substring(len - right); + return prefix + stars + suffix; + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java b/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java new file mode 100644 index 000000000..66eaee3e8 --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/port/in/FeedNotificationDispatchUseCase.java @@ -0,0 +1,17 @@ +package konkuk.thip.message.application.port.in; + +import konkuk.thip.message.adapter.out.event.dto.FeedEvents; + +public interface FeedNotificationDispatchUseCase { + void handleFollower(FeedEvents.FollowerEvent e); + + void handleFeedCommented(FeedEvents.FeedCommentedEvent e); + + void handleFeedCommentReplied(FeedEvents.FeedCommentRepliedEvent e); + + void handleFolloweeNewPost(FeedEvents.FolloweeNewPostEvent e); + + void handleFeedLiked(FeedEvents.FeedLikedEvent e); + + void handleFeedCommentLiked(FeedEvents.FeedCommentLikedEvent e); +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/port/in/RoomNotificationDispatchUseCase.java b/src/main/java/konkuk/thip/message/application/port/in/RoomNotificationDispatchUseCase.java new file mode 100644 index 000000000..2e3ac389d --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/port/in/RoomNotificationDispatchUseCase.java @@ -0,0 +1,23 @@ +package konkuk.thip.message.application.port.in; + +import konkuk.thip.message.adapter.out.event.dto.RoomEvents; + +public interface RoomNotificationDispatchUseCase { + void handleRoomPostCommented(RoomEvents.RoomPostCommentedEvent e); + + void handleRoomVoteStarted(RoomEvents.RoomVoteStartedEvent e); + + void handleRoomRecordCreated(RoomEvents.RoomRecordCreatedEvent e); + + void handleRoomRecruitClosedEarly(RoomEvents.RoomRecruitClosedEarlyEvent e); + + void handleRoomActivityStarted(RoomEvents.RoomActivityStartedEvent e); + + void handleRoomJoinRequestedToOwner(RoomEvents.RoomJoinRequestedToOwnerEvent e); + + void handleRoomCommentLiked(RoomEvents.RoomCommentLikedEvent e); + + void handleRoomPostLiked(RoomEvents.RoomPostLikedEvent e); + + void handleRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent e); +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java b/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java new file mode 100644 index 000000000..0fbc5631d --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/port/out/FeedEventCommandPort.java @@ -0,0 +1,27 @@ +package konkuk.thip.message.application.port.out; + +public interface FeedEventCommandPort { + + // λˆ„κ΅°κ°€ λ‚˜λ₯Ό νŒ”λ‘œμš°ν•˜λŠ” 경우 + void publishFollowEvent(Long targetUserId, Long actorUserId, String actorUsername); + + // λˆ„κ΅°κ°€ λ‚΄ ν”Όλ“œμ— λŒ“κΈ€μ„ λ‹€λŠ” 경우 + void publishFeedCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); + + // λˆ„κ΅°κ°€ λ‚΄ λŒ“κΈ€μ— λŒ€λŒ“κΈ€μ„ λ‹€λŠ” 경우 + void publishFeedRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); + + // λ‚΄κ°€ νŒ”λ‘œμš°ν•˜λŠ” μ‚¬λžŒμ΄ μƒˆ 글을 μ˜¬λ¦¬λŠ” 경우 + void publishFolloweeNewPostEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); + + // λ‚΄ ν”Όλ“œκ°€ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + void publishFeedLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); + + // λ‚΄ ν”Όλ“œ λŒ“κΈ€μ΄ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + void publishFeedCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long feedId); +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/port/out/FirebaseMessagingPort.java b/src/main/java/konkuk/thip/message/application/port/out/FirebaseMessagingPort.java new file mode 100644 index 000000000..f0acd88ee --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/port/out/FirebaseMessagingPort.java @@ -0,0 +1,11 @@ +package konkuk.thip.message.application.port.out; + +import com.google.firebase.messaging.Message; + +import java.util.List; + +public interface FirebaseMessagingPort { + void send(Message message, String fcmToken, String deviceId); + + void sendBatch(List messages, List fcmTokens, List deviceIds); +} diff --git a/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java b/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java new file mode 100644 index 000000000..b840969a1 --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/port/out/RoomEventCommandPort.java @@ -0,0 +1,38 @@ +package konkuk.thip.message.application.port.out; + +public interface RoomEventCommandPort { + + // λ‚΄ λͺ¨μž„λ°© 기둝/νˆ¬ν‘œμ— λŒ“κΈ€μ΄ 달린 경우 + void publishRoomPostCommentedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„방에 μƒˆλ‘œμš΄ νˆ¬ν‘œκ°€ μ‹œμž‘λœ 경우 + void publishRoomVoteStartedEvent(Long targetUserId, Long roomId, String roomTitle, + Integer page, Long postId); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„방에 μƒˆλ‘œμš΄ 기둝이 μž‘μ„±λœ 경우 + void publishRoomRecordCreatedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, String roomTitle, Integer page, Long postId); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„방이 μ‘°κΈ° μ’…λ£Œλœ 경우 (ν˜ΈμŠ€νŠΈκ°€ λͺ¨μ§‘ 마감 λ²„νŠΌ λˆ„λ₯Έ 경우) + void publishRoomRecruitClosedEarlyEvent(Long targetUserId, Long roomId, String roomTitle); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°© ν™œλ™μ΄ μ‹œμž‘λœ 경우 (방이 μ‹œμž‘ 기간이 λ˜μ–΄ μžλ™μœΌλ‘œ μ‹œμž‘λœ 경우) + void publishRoomActivityStartedEvent(Long targetUserId, Long roomId, String roomTitle); + + // λ‚΄κ°€ λ°©μž₯일 λ•Œ, μƒˆλ‘œμš΄ μ‚¬μš©μžκ°€ λͺ¨μž„λ°© μ°Έμ—¬λ₯Ό ν•œ 경우 + void publishRoomJoinEventToHost(Long hostUserId, Long roomId, String roomTitle, + Long actorUserId, String actorUsername); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°©μ˜ λ‚˜μ˜ λŒ“κΈ€μ΄ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + void publishRoomCommentLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°© μ•ˆμ˜ λ‚˜μ˜ 기둝/νˆ¬ν‘œκ°€ μ’‹μ•„μš”λ₯Ό λ°›λŠ” 경우 + void publishRoomPostLikedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); + + // λ‚΄κ°€ μ°Έμ—¬ν•œ λͺ¨μž„λ°©μ˜ λ‚˜μ˜ λŒ“κΈ€μ— λŒ€λŒ“κΈ€μ΄ 달린 경우 + void publishRoomPostCommentRepliedEvent(Long targetUserId, Long actorUserId, String actorUsername, + Long roomId, Integer page, Long postId, String postType); +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java b/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java new file mode 100644 index 000000000..27ecdcd7b --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/service/FeedNotificationDispatchService.java @@ -0,0 +1,127 @@ +package konkuk.thip.message.application.service; + +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import konkuk.thip.message.application.port.in.FeedNotificationDispatchUseCase; +import konkuk.thip.message.application.port.out.FirebaseMessagingPort; +import konkuk.thip.message.adapter.out.event.dto.FeedEvents; +import konkuk.thip.message.domain.NotificationCategory; +import konkuk.thip.message.domain.MessageRoute; +import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; +import konkuk.thip.notification.domain.FcmToken; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FeedNotificationDispatchService implements FeedNotificationDispatchUseCase { + + private final FcmTokenPersistencePort fcmTokenPersistencePort; + private final FirebaseMessagingPort firebasePort; + + @Override + public void handleFollower(final FeedEvents.FollowerEvent event) { + Notification n = buildNotification("νŒ”λ‘œμ›Œ μ•Œλ¦Ό", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚˜λ₯Ό λ±ν–ˆμ–΄μš”!"); + + List tokens = fcmTokenPersistencePort.findEnabledByUserId(event.targetUserId()); + + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), n, + MessageRoute.FEED_USER, + "userId", String.valueOf(event.actorUserId())); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + @Override + public void handleFeedCommented(final FeedEvents.FeedCommentedEvent event) { + Notification notification = buildNotification("μƒˆλ‘œμš΄ λŒ“κΈ€μ΄ λ‹¬λ Έμ–΄μš”", + "@" +event.actorUsername() + " λ‹˜μ΄ λ‚΄ 글에 λŒ“κΈ€μ„ λ‹¬μ•˜μ–΄μš”!"); + + pushFeedDetail(event.targetUserId(), notification, event.feedId()); + } + + @Override + public void handleFeedCommentReplied(final FeedEvents.FeedCommentRepliedEvent event) { + Notification notification = buildNotification("μƒˆλ‘œμš΄ 닡글이 λ‹¬λ Έμ–΄μš”", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚΄ λŒ“κΈ€μ— 닡글을 λ‹¬μ•˜μ–΄μš”!"); + + pushFeedDetail(event.targetUserId(), notification, event.feedId()); + } + + @Override + public void handleFolloweeNewPost(final FeedEvents.FolloweeNewPostEvent event) { + Notification notification = buildNotification("μƒˆ κΈ€ μ•Œλ¦Ό", + "@" + event.actorUsername() + " λ‹˜μ΄ μƒˆλ‘œμš΄ 글을 μž‘μ„±ν–ˆμ–΄μš”!"); + + pushFeedDetail(event.targetUserId(), notification, event.feedId()); + } + + @Override + public void handleFeedLiked(final FeedEvents.FeedLikedEvent event) { + Notification notification = buildNotification("λ‚΄ 글을 μ’‹μ•„ν•©λ‹ˆλ‹€", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚΄ 글에 μ’‹μ•„μš”λ₯Ό λˆŒλ €μ–΄μš”!"); + + pushFeedDetail(event.targetUserId(), notification, event.feedId()); + } + + @Override + public void handleFeedCommentLiked(final FeedEvents.FeedCommentLikedEvent event) { + Notification notification = buildNotification("μ’‹μ•„μš” μ•Œλ¦Ό", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚΄ λŒ“κΈ€μ— μ’‹μ•„μš”λ₯Ό λˆŒλ €μ–΄μš”!"); + + pushFeedDetail(event.targetUserId(), notification, event.feedId()); + } + + private void pushFeedDetail(Long userId, Notification notification, Long feedId) { + List tokens = fcmTokenPersistencePort.findEnabledByUserId(userId); + + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.FEED_DETAIL, + "feedId", String.valueOf(feedId)); + + msgs.add(m); + tk.add(t.getFcmToken()); + dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + private Notification buildNotification(final String title, final String body) { + return Notification.builder().setTitle(NotificationCategory.FEED.prefixedTitle(title)).setBody(body).build(); + } + + private Message buildMessage(final String token, final Notification n, + final MessageRoute route, + final String... kv) { + Message.Builder b = Message.builder() + .setToken(token) + .setNotification(n) + .putData("category", NotificationCategory.FEED.getDisplay()) + .putData("action", "OPEN_ROUTE") + .putData("route", route.getCode()); + for (int i = 0; i + 1 < kv.length; i += 2) b.putData(kv[i], kv[i + 1]); + return b.build(); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java b/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java new file mode 100644 index 000000000..93acf0b81 --- /dev/null +++ b/src/main/java/konkuk/thip/message/application/service/RoomNotificationDispatchService.java @@ -0,0 +1,261 @@ +package konkuk.thip.message.application.service; + +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import konkuk.thip.message.application.port.in.RoomNotificationDispatchUseCase; +import konkuk.thip.message.application.port.out.FirebaseMessagingPort; +import konkuk.thip.message.adapter.out.event.dto.RoomEvents; +import konkuk.thip.message.domain.NotificationCategory; +import konkuk.thip.message.domain.MessageRoute; +import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; +import konkuk.thip.notification.domain.FcmToken; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RoomNotificationDispatchService implements RoomNotificationDispatchUseCase { + + private final FcmTokenPersistencePort fcmTokenQueryPort; + private final FirebaseMessagingPort firebasePort; + + @Override + public void handleRoomPostCommented(final RoomEvents.RoomPostCommentedEvent event) { + Notification notification = buildNotification("μƒˆλ‘œμš΄ λŒ“κΈ€μ΄ λ‹¬λ Έμ–΄μš”", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚΄ λ…μ„œκΈ°λ‘μ— λŒ“κΈ€μ„ λ‹¬μ•˜μ–΄μš”!"); + + List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_POST_DETAIL, + "roomId", String.valueOf(event.roomId()), + "page", String.valueOf(event.page()), + "type", "group", + "postId", String.valueOf(event.postId()), + "postType", String.valueOf(event.postType())); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + @Override + public void handleRoomVoteStarted(final RoomEvents.RoomVoteStartedEvent event) { + Notification notification = buildNotification(event.roomTitle(), + "μƒˆλ‘œμš΄ νˆ¬ν‘œκ°€ μ‹œμž‘λ˜μ—ˆμ–΄μš”!"); + + List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_VOTE_DETAIL, + "roomId", String.valueOf(event.roomId()), + "page", String.valueOf(event.page()), + "type", "group", + "postId", String.valueOf(event.postId()), + "postType", "VOTE"); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + @Override + public void handleRoomRecordCreated(final RoomEvents.RoomRecordCreatedEvent event) { + Notification notification = buildNotification(event.roomTitle(), + "@" + event.actorUsername() + " λ‹˜μ΄ μƒˆλ‘œμš΄ λ…μ„œ 기둝을 μž‘μ„±ν–ˆμ–΄μš”!"); + + List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_RECORD_DETAIL, + "roomId", String.valueOf(event.roomId()), + "page", String.valueOf(event.page()), + "type", "group", + "postId", String.valueOf(event.postId()), + "postType", "RECORD"); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + @Override + public void handleRoomRecruitClosedEarly(final RoomEvents.RoomRecruitClosedEarlyEvent event) { + Notification n = buildNotification(event.roomTitle(), + "λͺ¨μž„λ°© ν™œλ™μ΄ μ‹œμž‘λ˜μ—ˆμ–΄μš”. λͺ¨μž„λ°©μ—μ„œ λ…μ„œ 기둝을 μ‹œμž‘ν•΄λ³΄μ„Έμš”!"); + + pushRoomMain(event.targetUserId(), event.roomId(), n); + } + + @Override + public void handleRoomActivityStarted(final RoomEvents.RoomActivityStartedEvent event) { + Notification notification = buildNotification(event.roomTitle(), + "λͺ¨μž„λ°© ν™œλ™μ΄ μ‹œμž‘λ˜μ—ˆμ–΄μš”. λͺ¨μž„λ°©μ—μ„œ λ…μ„œ 기둝을 μ‹œμž‘ν•΄λ³΄μ„Έμš”!"); + + pushRoomMain(event.targetUserId(), event.roomId(), notification); + } + + @Override + public void handleRoomJoinRequestedToOwner(final RoomEvents.RoomJoinRequestedToOwnerEvent event) { + Notification n = buildNotification(event.roomTitle(), + "@" + event.applicantUsername() + " λ‹˜μ΄ λͺ¨μž„에 μ°Έμ—¬ν–ˆμ–΄μš”!"); + + pushRoomDetail(event.ownerUserId(), event.roomId(), n); + } + + @Override + public void handleRoomCommentLiked(final RoomEvents.RoomCommentLikedEvent event) { + Notification notification = buildNotification("λ‚΄ λŒ“κΈ€μ„ μ’‹μ•„ν•©λ‹ˆλ‹€", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚΄ λŒ“κΈ€μ— μ’‹μ•„μš”λ₯Ό λˆŒλ €μ–΄μš”!"); + + List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_POST_DETAIL, + "roomId", String.valueOf(event.roomId()), + "page", String.valueOf(event.page()), + "type", "group", + "postId", String.valueOf(event.postId()), + "postType", String.valueOf(event.postType())); + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + @Override + public void handleRoomPostLiked(final RoomEvents.RoomPostLikedEvent event) { + Notification notification = buildNotification("μ’‹μ•„μš” μ•Œλ¦Ό", + "@" + event.actorUsername() + " λ‹˜μ΄ λ‚΄ λ…μ„œκΈ°λ‘μ— μ’‹μ•„μš”λ₯Ό λˆŒλ €μ–΄μš”!"); + + List tokens = fcmTokenQueryPort.findEnabledByUserId(event.targetUserId()); + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_POST_DETAIL, + "roomId", String.valueOf(event.roomId()), + "page", String.valueOf(event.page()), + "type", "group", + "postId", String.valueOf(event.postId()), + "postType", String.valueOf(event.postType())); + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + @Override + public void handleRoomPostCommentReplied(RoomEvents.RoomPostCommentRepliedEvent e) { + Notification notification = buildNotification("μƒˆλ‘œμš΄ λŒ“κΈ€μ΄ λ‹¬λ Έμ–΄μš”", + "@" + e.actorUsername() + " λ‹˜μ΄ λ‚΄ λŒ“κΈ€μ— λŒ€λŒ“κΈ€μ„ λ‹¬μ•˜μ–΄μš”!"); + + List tokens = fcmTokenQueryPort.findEnabledByUserId(e.targetUserId()); + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_POST_DETAIL, + "roomId", String.valueOf(e.roomId()), + "page", String.valueOf(e.page()), + "type", "group", + "postId", String.valueOf(e.postId()), + "postType", String.valueOf(e.postType())); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + // ===== helpers ===== + + private void pushRoomMain(Long targetUserId, Long roomId, Notification notification) { + List tokens = fcmTokenQueryPort.findEnabledByUserId(targetUserId); + + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_MAIN, + "roomId", String.valueOf(roomId)); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + private void pushRoomDetail(Long targetUserId, Long roomId, Notification notification) { + List tokens = fcmTokenQueryPort.findEnabledByUserId(targetUserId); + + if (tokens.isEmpty()) return; + + List msgs = new ArrayList<>(tokens.size()); + List tk = new ArrayList<>(tokens.size()); + List dev = new ArrayList<>(tokens.size()); + + for (FcmToken t : tokens) { + Message m = buildMessage(t.getFcmToken(), notification, + MessageRoute.ROOM_DETAIL, + "roomId", String.valueOf(roomId)); + + msgs.add(m); tk.add(t.getFcmToken()); dev.add(t.getDeviceId()); + } + firebasePort.sendBatch(msgs, tk, dev); + } + + private Notification buildNotification(final String title, final String body) { + return Notification.builder().setTitle(NotificationCategory.ROOM.prefixedTitle(title)).setBody(body).build(); + } + + private Message buildMessage(final String token, final Notification n, + final MessageRoute route, + final String... kv) { + var b = Message.builder() + .setToken(token) + .setNotification(n) + .putData("category", NotificationCategory.ROOM.getDisplay()) + .putData("action", "OPEN_ROUTE") + .putData("route", route.getCode()); + for (int i = 0; i + 1 < kv.length; i += 2) b.putData(kv[i], kv[i + 1]); + return b.build(); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/domain/MessageRoute.java b/src/main/java/konkuk/thip/message/domain/MessageRoute.java new file mode 100644 index 000000000..d664ecc8f --- /dev/null +++ b/src/main/java/konkuk/thip/message/domain/MessageRoute.java @@ -0,0 +1,21 @@ +package konkuk.thip.message.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MessageRoute { + // FEED + FEED_USER("FEED_USER"), // μžμ‹ μ„ νŒ”λ‘œμš°ν•œ μ‚¬μš©μžμ˜ ν”Όλ“œ λͺ©λ‘μœΌλ‘œ ν™”λ©΄ 이동 + FEED_DETAIL("FEED_DETAIL"), // νŠΉμ • ν”Όλ“œ 상세 ν™”λ©΄μœΌλ‘œ 이동 + + // ROOM + ROOM_MAIN("ROOM_MAIN"), // νŠΉμ • λͺ¨μž„λ°© 메인 ν™”λ©΄μœΌλ‘œ 이동 + ROOM_DETAIL("ROOM_DETAIL"), // νŠΉμ • λͺ¨μž„ 상세정보 ν™”λ©΄μœΌλ‘œ 이동 + ROOM_POST_DETAIL("ROOM_POST_DETAIL"), // νŠΉμ • λͺ¨μž„ κ²Œμ‹œκΈ€ 상세 ν™”λ©΄μœΌλ‘œ 이동 -> PostType으둜 νˆ¬ν‘œμΈμ§€ 기둝인지 νŒλ‹¨ + ROOM_RECORD_DETAIL("ROOM_RECORD_DETAIL"), // νŠΉμ • λͺ¨μž„ 기둝 상세 ν™”λ©΄μœΌλ‘œ 이동 (기둝μž₯ 쑰회 - νŽ˜μ΄μ§€ ν•„ν„° κ±Έλ¦°μ±„λ‘œ) + ROOM_VOTE_DETAIL("ROOM_VOTE_DETAIL"); // νŠΉμ • λͺ¨μž„ νˆ¬ν‘œ 상세 ν™”λ©΄μœΌλ‘œ 이동 (νˆ¬ν‘œ 쑰회 - νŽ˜μ΄μ§€ ν•„ν„° κ±Έλ¦°μ±„λ‘œ) + + private final String code; +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/message/domain/NotificationCategory.java b/src/main/java/konkuk/thip/message/domain/NotificationCategory.java new file mode 100644 index 000000000..3280395a2 --- /dev/null +++ b/src/main/java/konkuk/thip/message/domain/NotificationCategory.java @@ -0,0 +1,17 @@ +package konkuk.thip.message.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationCategory { + FEED("ν”Όλ“œ"), + ROOM("λͺ¨μž„"); + + private final String display; + + public String prefixedTitle(String title) { + return "[" + display + "] " + title; + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java index e27c11f9b..b6d3772f5 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationCommandController.java @@ -1,10 +1,63 @@ package konkuk.thip.notification.adapter.in.web; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.common.swagger.annotation.ExceptionDescription; +import konkuk.thip.notification.adapter.in.web.request.FcmTokenDeleteRequest; +import konkuk.thip.notification.adapter.in.web.request.FcmTokenEnableStateChangeRequest; +import konkuk.thip.notification.adapter.in.web.request.FcmTokenRegisterRequest; +import konkuk.thip.notification.adapter.in.web.response.FcmTokenEnableStateChangeResponse; +import konkuk.thip.notification.application.port.in.FcmDeleteUseCase; +import konkuk.thip.notification.application.port.in.FcmEnableStateChangeUseCase; +import konkuk.thip.notification.application.port.in.FcmRegisterUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Notification Command API", description = "μ•Œλ¦Ό κ΄€λ ¨ μƒνƒœλ³€κ²½ API") @RestController @RequiredArgsConstructor public class NotificationCommandController { + private final FcmRegisterUseCase fcmRegisterUseCase; + private final FcmEnableStateChangeUseCase fcmEnableStateChangeUseCase; + private final FcmDeleteUseCase fcmDeleteUseCase; + + @Operation(summary = "FCM 토큰 등둝", description = "μ‚¬μš©μžμ˜ FCM 토큰을 μ„œλ²„μ— λ“±λ‘ν•©λ‹ˆλ‹€. κΈ°μ‘΄ 토큰이 μžˆλ‹€λ©΄ deviceId κΈ°μ€€μœΌλ‘œ 토큰을 κ°±μ‹ ν•©λ‹ˆλ‹€.") + @PostMapping("/notifications/fcm-tokens") + @ExceptionDescription(FCM_TOKEN_REGISTER) + public BaseResponse registerFcmToken( + @RequestBody @Valid FcmTokenRegisterRequest request, + @Parameter(hidden = true) @UserId Long userId + ) { + fcmRegisterUseCase.registerToken(request.toCommand(userId)); + return BaseResponse.ok(null); + } + + @Operation(summary = "ν‘Έμ‹œ μ•Œλ¦Ό μˆ˜μ‹  μ—¬λΆ€ μ„€μ • λ³€κ²½", description = "μ‚¬μš©μžμ˜ ν‘Έμ‹œ μ•Œλ¦Ό μˆ˜μ‹  μ—¬λΆ€λ₯Ό λ³€κ²½ν•©λ‹ˆλ‹€.") + @ExceptionDescription(FCM_TOKEN_ENABLE_STATE_CHANGE) + @PatchMapping("/notifications/enable-state") + public BaseResponse updatePushNotificationSetting( + @RequestBody @Valid FcmTokenEnableStateChangeRequest request, + @Parameter(hidden = true) @UserId Long userId + ) { + return BaseResponse.ok( + FcmTokenEnableStateChangeResponse.of(fcmEnableStateChangeUseCase.changeEnableState(request.toCommand(userId)))); + } + + @Operation(summary = "FCM 토큰 μ‚­μ œ", description = "μ‚¬μš©μžμ˜ FCM 토큰을 μ‚­μ œν•©λ‹ˆλ‹€.") + @ExceptionDescription(FCM_TOKEN_DELETE) + @DeleteMapping("/notifications/fcm-tokens") + public BaseResponse deleteFcmToken( + @RequestBody @Valid FcmTokenDeleteRequest request, + @Parameter(hidden = true) @UserId Long userId + ) { + fcmDeleteUseCase.deleteToken(request.toCommand(userId)); + return BaseResponse.ok(null); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java new file mode 100644 index 000000000..a18419a16 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenDeleteRequest.java @@ -0,0 +1,19 @@ +package konkuk.thip.notification.adapter.in.web.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; + +@Schema(description = "ν‘Έμ‹œ μ•Œλ¦Ό μ„€μ • μ‚­μ œ μš”μ²­ DTO") +public record FcmTokenDeleteRequest( + @NotBlank + @Schema(description = "λ””λ°”μ΄μŠ€ 고유 ID", example = "device12345") + String deviceId +) { + public FcmTokenDeleteCommand toCommand(Long userId) { + return new FcmTokenDeleteCommand( + userId, + this.deviceId + ); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java new file mode 100644 index 000000000..767032e35 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenEnableStateChangeRequest.java @@ -0,0 +1,25 @@ +package konkuk.thip.notification.adapter.in.web.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; + +@Schema(description = "ν‘Έμ‹œ μ•Œλ¦Ό μ„€μ • λ³€κ²½ μš”μ²­ DTO") +public record FcmTokenEnableStateChangeRequest( + @NotNull + @Schema(description = "ν‘Έμ‹œ μ•Œλ¦Ό μˆ˜μ‹  μ—¬λΆ€", example = "true") + Boolean enable, + + @NotBlank + @Schema(description = "λ””λ°”μ΄μŠ€ 고유 ID", example = "device12345") + String deviceId +) { + public FcmEnableStateChangeCommand toCommand(Long userId) { + return new FcmEnableStateChangeCommand( + userId, + this.enable, + this.deviceId + ); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java new file mode 100644 index 000000000..69b516421 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/request/FcmTokenRegisterRequest.java @@ -0,0 +1,31 @@ +package konkuk.thip.notification.adapter.in.web.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import konkuk.thip.notification.domain.value.PlatformType; +import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; + +@Schema(description = "FCM 토큰 등둝 μš”μ²­ DTO") +public record FcmTokenRegisterRequest( + @NotBlank + @Schema(description = "λ””λ°”μ΄μŠ€ 고유 ID", example = "device12345") + String deviceId, + + @NotBlank + @Schema(description = "FCM 토큰", example = "fcm_token_example_123456") + String fcmToken, + + @NotNull + @Schema(description = "ν”Œλž«νΌ νƒ€μž… (ANDROID λ˜λŠ” WEB)", example = "ANDROID") + PlatformType platformType +) { + public FcmTokenRegisterCommand toCommand(Long userId) { + return FcmTokenRegisterCommand.builder() + .deviceId(this.deviceId) + .fcmToken(this.fcmToken) + .platformType(this.platformType) + .userId(userId) + .build(); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/request/UserSignupRequest.java b/src/main/java/konkuk/thip/notification/adapter/in/web/request/UserSignupRequest.java deleted file mode 100644 index d463dff6b..000000000 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/request/UserSignupRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.notification.adapter.in.web.request; - -import lombok.Getter; - -@Getter -public class UserSignupRequest { -} diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/DummyResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/DummyResponse.java deleted file mode 100644 index d5272cec1..000000000 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/response/DummyResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.notification.adapter.in.web.response; - -import lombok.Getter; - -@Getter -public class DummyResponse { -} diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/FcmTokenEnableStateChangeResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/FcmTokenEnableStateChangeResponse.java new file mode 100644 index 000000000..1a4ee24a8 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/FcmTokenEnableStateChangeResponse.java @@ -0,0 +1,9 @@ +package konkuk.thip.notification.adapter.in.web.response; + +public record FcmTokenEnableStateChangeResponse( + boolean isEnabled +) { + public static FcmTokenEnableStateChangeResponse of(boolean isEnabled) { + return new FcmTokenEnableStateChangeResponse(isEnabled); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java b/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java new file mode 100644 index 000000000..25ea5fc29 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/jpa/FcmTokenJpaEntity.java @@ -0,0 +1,52 @@ +package konkuk.thip.notification.adapter.out.jpa; + +import jakarta.persistence.*; +import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.notification.domain.FcmToken; +import konkuk.thip.notification.domain.value.PlatformType; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import lombok.*; + +import java.time.LocalDate; + +@Entity +@Table(name = "fcm_tokens") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class FcmTokenJpaEntity extends BaseJpaEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long fcmTokenId; + + @Column(name = "fcm_token", nullable = false) + private String fcmToken; + + @Column(name = "device_id", length = 128, nullable = false, unique = true) + private String deviceId; + + @Enumerated(EnumType.STRING) + @Column(name = "platform", length = 16, nullable = false) + private PlatformType platformType; + + @Column(name = "last_used_time", nullable = false) + private LocalDate lastUsedTime; + + @Column(name = "is_enabled", nullable = false) + private boolean isEnabled; // ν‘Έμ‰¬μ•Œλ¦Ό μˆ˜μ‹  μ—¬λΆ€ + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private UserJpaEntity userJpaEntity; + + public void updateFrom(FcmToken fcmToken, UserJpaEntity userJpaEntity) { + this.fcmToken = fcmToken.getFcmToken(); + this.platformType = fcmToken.getPlatformType(); + this.lastUsedTime = fcmToken.getLastUsedTime(); + this.userJpaEntity = userJpaEntity; + + this.isEnabled = fcmToken.isEnabled(); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/mapper/FcmTokenMapper.java b/src/main/java/konkuk/thip/notification/adapter/out/mapper/FcmTokenMapper.java new file mode 100644 index 000000000..1acb141bf --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/mapper/FcmTokenMapper.java @@ -0,0 +1,36 @@ +package konkuk.thip.notification.adapter.out.mapper; + +import konkuk.thip.notification.adapter.out.jpa.FcmTokenJpaEntity; +import konkuk.thip.notification.domain.FcmToken; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import org.springframework.stereotype.Component; + +@Component +public class FcmTokenMapper { + + public FcmTokenJpaEntity toJpaEntity(FcmToken fcmToken, UserJpaEntity userJpaEntity) { + return FcmTokenJpaEntity.builder() + .fcmToken(fcmToken.getFcmToken()) + .deviceId(fcmToken.getDeviceId()) + .platformType(fcmToken.getPlatformType()) + .lastUsedTime(fcmToken.getLastUsedTime()) + .isEnabled(fcmToken.isEnabled()) + .userJpaEntity(userJpaEntity) + .build(); + } + + public FcmToken toDomainEntity(FcmTokenJpaEntity fcmTokenJpaEntity) { + return FcmToken.builder() + .id(fcmTokenJpaEntity.getFcmTokenId()) + .fcmToken(fcmTokenJpaEntity.getFcmToken()) + .deviceId(fcmTokenJpaEntity.getDeviceId()) + .platformType(fcmTokenJpaEntity.getPlatformType()) + .lastUsedTime(fcmTokenJpaEntity.getLastUsedTime()) + .isEnabled(fcmTokenJpaEntity.isEnabled()) + .userId(fcmTokenJpaEntity.getUserJpaEntity().getUserId()) + .createdAt(fcmTokenJpaEntity.getCreatedAt()) + .modifiedAt(fcmTokenJpaEntity.getModifiedAt()) + .status(fcmTokenJpaEntity.getStatus()) + .build(); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java new file mode 100644 index 000000000..8c30fd173 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/FcmTokenPersistencePersistenceAdapter.java @@ -0,0 +1,64 @@ +package konkuk.thip.notification.adapter.out.persistence; + +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.notification.adapter.out.jpa.FcmTokenJpaEntity; +import konkuk.thip.notification.adapter.out.mapper.FcmTokenMapper; +import konkuk.thip.notification.adapter.out.persistence.repository.FcmTokenJpaRepository; +import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; +import konkuk.thip.notification.domain.FcmToken; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class FcmTokenPersistencePersistenceAdapter implements FcmTokenPersistencePort { + + private final FcmTokenJpaRepository fcmTokenJpaRepository; + private final UserJpaRepository userJpaRepository; + + private final FcmTokenMapper fcmTokenMapper; + + @Override + public Optional findByDeviceId(String deviceId) { + return fcmTokenJpaRepository.findByDeviceId(deviceId) + .map(fcmTokenMapper::toDomainEntity); + } + + @Override + public FcmToken save(FcmToken token) { + UserJpaEntity user = userJpaRepository.findByUserId(token.getUserId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND)); + + var saved = fcmTokenJpaRepository.save(fcmTokenMapper.toJpaEntity(token, user)); + return fcmTokenMapper.toDomainEntity(saved); + } + + @Override + public void update(FcmToken fcmToken) { + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(fcmToken.getUserId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.USER_NOT_FOUND)); + + FcmTokenJpaEntity fcmTokenJpaEntity = fcmTokenJpaRepository.findByFcmTokenId(fcmToken.getId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FCM_TOKEN_NOT_FOUND)); + + fcmTokenJpaEntity.updateFrom(fcmToken, userJpaEntity); + fcmTokenJpaRepository.save(fcmTokenJpaEntity); + } + + @Override + public List findEnabledByUserId(Long userId) { + return fcmTokenJpaRepository.findByUserIdAndIsEnabledTrue(userId).stream() + .map(fcmTokenMapper::toDomainEntity).toList(); + } + + @Override + public void deleteByUserIdAndDeviceId(Long userId, String deviceId) { + fcmTokenJpaRepository.deleteByUserIdAndDeviceId(userId, deviceId); + } +} diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/FcmTokenJpaRepository.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/FcmTokenJpaRepository.java new file mode 100644 index 000000000..f7f0d09c7 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/FcmTokenJpaRepository.java @@ -0,0 +1,24 @@ +package konkuk.thip.notification.adapter.out.persistence.repository; + +import konkuk.thip.notification.adapter.out.jpa.FcmTokenJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface FcmTokenJpaRepository extends JpaRepository { + + @Query("SELECT f FROM FcmTokenJpaEntity f WHERE f.deviceId = :deviceId") + Optional findByDeviceId(String deviceId); + + @Query("SELECT f FROM FcmTokenJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.isEnabled = true") + List findByUserIdAndIsEnabledTrue(Long userId); + + Optional findByFcmTokenId(Long id); + + @Modifying + @Query("DELETE FROM FcmTokenJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.deviceId = :deviceId") + void deleteByUserIdAndDeviceId(Long userId, String deviceId); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java new file mode 100644 index 000000000..b6335003b --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/FcmDeleteUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.notification.application.port.in; + +import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; + +public interface FcmDeleteUseCase { + void deleteToken(FcmTokenDeleteCommand command); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java new file mode 100644 index 000000000..14ba4c395 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/FcmEnableStateChangeUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.notification.application.port.in; + +import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; + +public interface FcmEnableStateChangeUseCase { + boolean changeEnableState(FcmEnableStateChangeCommand command); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java new file mode 100644 index 000000000..ff9b096ed --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/FcmRegisterUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.notification.application.port.in; + +import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; + +public interface FcmRegisterUseCase { + void registerToken(FcmTokenRegisterCommand command); +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyCommand.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyCommand.java deleted file mode 100644 index c5c44a118..000000000 --- a/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyCommand.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.notification.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyCommand { - -} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyQuery.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyQuery.java deleted file mode 100644 index 1b8879fd7..000000000 --- a/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyQuery.java +++ /dev/null @@ -1,9 +0,0 @@ -package konkuk.thip.notification.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyQuery { -} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyResult.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyResult.java deleted file mode 100644 index 4f6005404..000000000 --- a/src/main/java/konkuk/thip/notification/application/port/in/dto/DummyResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.notification.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class DummyResult { - -} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmEnableStateChangeCommand.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmEnableStateChangeCommand.java new file mode 100644 index 000000000..dd5942b07 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmEnableStateChangeCommand.java @@ -0,0 +1,8 @@ +package konkuk.thip.notification.application.port.in.dto; + +public record FcmEnableStateChangeCommand( + Long userId, + boolean enable, + String deviceId +) { +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenDeleteCommand.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenDeleteCommand.java new file mode 100644 index 000000000..4c977031e --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenDeleteCommand.java @@ -0,0 +1,7 @@ +package konkuk.thip.notification.application.port.in.dto; + +public record FcmTokenDeleteCommand( + Long userId, + String deviceId +) { +} diff --git a/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java new file mode 100644 index 000000000..d25e8be28 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/dto/FcmTokenRegisterCommand.java @@ -0,0 +1,13 @@ +package konkuk.thip.notification.application.port.in.dto; + +import konkuk.thip.notification.domain.value.PlatformType; +import lombok.Builder; + +@Builder +public record FcmTokenRegisterCommand( + String deviceId, + String fcmToken, + PlatformType platformType, + Long userId +) { +} diff --git a/src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java b/src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java new file mode 100644 index 000000000..0c4523685 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/out/FcmTokenPersistencePort.java @@ -0,0 +1,25 @@ +package konkuk.thip.notification.application.port.out; + +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.notification.domain.FcmToken; + +import java.util.List; +import java.util.Optional; + +public interface FcmTokenPersistencePort { + Optional findByDeviceId(String deviceId); + + default FcmToken getByDeviceIdOrThrow(String deviceId) { + return findByDeviceId(deviceId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FCM_TOKEN_NOT_FOUND)); + } + + FcmToken save(FcmToken token); + + void update(FcmToken fcmToken); + + List findEnabledByUserId(Long userId); + + void deleteByUserIdAndDeviceId(Long userId, String deviceId); +} diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java b/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java new file mode 100644 index 000000000..b08f5ab45 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/FcmDeleteService.java @@ -0,0 +1,25 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.application.port.in.FcmDeleteUseCase; +import konkuk.thip.notification.application.port.in.dto.FcmTokenDeleteCommand; +import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; +import konkuk.thip.notification.domain.FcmToken; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class FcmDeleteService implements FcmDeleteUseCase { + + private final FcmTokenPersistencePort fcmTokenPersistencePort; + + @Override + @Transactional + public void deleteToken(FcmTokenDeleteCommand command) { + FcmToken fcmToken = fcmTokenPersistencePort.getByDeviceIdOrThrow(command.deviceId()); + fcmToken.validateFcmOwner(command.userId()); + + fcmTokenPersistencePort.deleteByUserIdAndDeviceId(command.userId(), command.deviceId()); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java b/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java new file mode 100644 index 000000000..abae5af41 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/FcmEnableStateChangeService.java @@ -0,0 +1,27 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.application.port.in.FcmEnableStateChangeUseCase; +import konkuk.thip.notification.application.port.in.dto.FcmEnableStateChangeCommand; +import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; +import konkuk.thip.notification.domain.FcmToken; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class FcmEnableStateChangeService implements FcmEnableStateChangeUseCase { + + private final FcmTokenPersistencePort fcmTokenPersistencePort; + + @Override + @Transactional + public boolean changeEnableState(FcmEnableStateChangeCommand command) { + FcmToken fcmToken = fcmTokenPersistencePort.getByDeviceIdOrThrow(command.deviceId()); + + fcmToken.changeEnableState(command.enable(), command.userId()); + fcmTokenPersistencePort.update(fcmToken); + + return command.enable(); + } +} diff --git a/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java b/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java new file mode 100644 index 000000000..71f8835d2 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/FcmRegisterService.java @@ -0,0 +1,47 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.application.port.in.FcmRegisterUseCase; +import konkuk.thip.notification.application.port.in.dto.FcmTokenRegisterCommand; +import konkuk.thip.notification.application.port.out.FcmTokenPersistencePort; +import konkuk.thip.notification.domain.FcmToken; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +public class FcmRegisterService implements FcmRegisterUseCase { + + private final FcmTokenPersistencePort fcmTokenPersistencePort; + + @Override + @Transactional + public void registerToken(FcmTokenRegisterCommand command) { + // 같은 λ””λ°”μ΄μŠ€λ‘œ λ“±λ‘λœ 토큰이 μžˆλŠ”μ§€ 확인 + fcmTokenPersistencePort.findByDeviceId(command.deviceId()).ifPresentOrElse( + existingToken -> { + // 있으면 μƒˆλ‘œμš΄ κ³„μ •μœΌλ‘œ ꡐ체 λ˜λŠ” κΈ°μ‘΄ κ³„μ •μ˜ 토큰 κ°±μ‹  + existingToken.updateToken( + command.fcmToken(), + command.platformType(), + LocalDate.now(), + command.userId() + ); + fcmTokenPersistencePort.update(existingToken); + }, + () -> { + // μ—†μœΌλ©΄ μƒˆλ‘œ 등둝 + fcmTokenPersistencePort.save(FcmToken.withoutId( + command.fcmToken(), + command.deviceId(), + command.platformType(), + LocalDate.now(), + true, + command.userId() + )); + } + ); + } +} diff --git a/src/main/java/konkuk/thip/notification/domain/FcmToken.java b/src/main/java/konkuk/thip/notification/domain/FcmToken.java new file mode 100644 index 000000000..58bc299db --- /dev/null +++ b/src/main/java/konkuk/thip/notification/domain/FcmToken.java @@ -0,0 +1,74 @@ +package konkuk.thip.notification.domain; + +import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.notification.domain.value.PlatformType; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; + +@Getter +@SuperBuilder +public class FcmToken extends BaseDomainEntity { + + private Long id; + + private String fcmToken; + + private String deviceId; + + private PlatformType platformType; + + private LocalDate lastUsedTime; + + private boolean isEnabled; // ν‘Έμ‰¬μ•Œλ¦Ό μˆ˜μ‹  μ—¬λΆ€ + + private Long userId; + + public static FcmToken withoutId ( + String fcmToken, + String deviceId, + PlatformType platformType, + LocalDate lastUsedTime, + boolean isEnabled, + Long userId + ) { + return FcmToken.builder() + .fcmToken(fcmToken) + .deviceId(deviceId) + .platformType(platformType) + .lastUsedTime(lastUsedTime) + .isEnabled(isEnabled) + .userId(userId) + .build(); + } + + // 토큰 κ°±μ‹  + public void updateToken(String fcmToken, PlatformType platformType, LocalDate lastUsedTime, Long userId) { + this.fcmToken = fcmToken; + this.platformType = platformType; + this.lastUsedTime = lastUsedTime; + this.userId = userId; + } + + public void changeEnableState(boolean enable, long actorUserId) { + validateChangeEnableState(enable, actorUserId); + this.isEnabled = enable; + } + + private void validateChangeEnableState(boolean enable, long actorUserId) { + if (this.isEnabled == enable) { + throw new InvalidStateException(ErrorCode.FCM_TOKEN_ENABLED_STATE_ALREADY, + new IllegalArgumentException("이미 " + (enable ? "ν™œμ„±ν™”" : "λΉ„ν™œμ„±ν™”") + "된 μƒνƒœμž…λ‹ˆλ‹€.")); + } + validateFcmOwner(actorUserId); + } + + public void validateFcmOwner(long actorUserId) { + if (this.userId != actorUserId) { + throw new InvalidStateException(ErrorCode.FCM_TOKEN_ACCESS_FORBIDDEN); + } + } +} diff --git a/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java b/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java new file mode 100644 index 000000000..5ae99334f --- /dev/null +++ b/src/main/java/konkuk/thip/notification/domain/value/PlatformType.java @@ -0,0 +1,5 @@ +package konkuk.thip.notification.domain.value; + +public enum PlatformType { + ANDROID, WEB +} diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java index b7d61e7d8..a7f8e8305 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java @@ -1,6 +1,7 @@ package konkuk.thip.post.adapter.out.persistence; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java index cd3975869..51c10edbe 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java @@ -1,5 +1,6 @@ package konkuk.thip.post.adapter.out.persistence; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.post.application.port.out.PostLikeQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java new file mode 100644 index 000000000..966a04f04 --- /dev/null +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostQueryPersistenceAdapter.java @@ -0,0 +1,30 @@ +package konkuk.thip.post.adapter.out.persistence; + +import konkuk.thip.post.adapter.out.persistence.repository.PostJpaRepository; +import konkuk.thip.post.application.port.out.PostQueryPort; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PostQueryPersistenceAdapter implements PostQueryPort { + + private final PostJpaRepository postJpaRepository; + + @Override + public PostQueryDto getPostQueryDtoByFeedId(Long feedId) { + return postJpaRepository.getPostQueryDtoByFeedId(feedId); + } + + @Override + public PostQueryDto getPostQueryDtoByRecordId(Long recordId) { + return postJpaRepository.getPostQueryDtoByRecordId(recordId); + } + + @Override + public PostQueryDto getPostQueryDtoByVoteId(Long voteId) { + return postJpaRepository.getPostQueryDtoByVoteId(voteId); + } + +} diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java new file mode 100644 index 000000000..2e7b0f394 --- /dev/null +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostJpaRepository.java @@ -0,0 +1,9 @@ +package konkuk.thip.post.adapter.out.persistence.repository; + +import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostJpaRepository extends JpaRepository, PostQueryRepository { +} diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostLikeJpaRepository.java similarity index 97% rename from src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java rename to src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostLikeJpaRepository.java index 6eeee5f44..beb05676e 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostLikeJpaRepository.java @@ -1,4 +1,4 @@ -package konkuk.thip.post.adapter.out.persistence; +package konkuk.thip.post.adapter.out.persistence.repository; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepository.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepository.java new file mode 100644 index 000000000..fd7890345 --- /dev/null +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepository.java @@ -0,0 +1,10 @@ +package konkuk.thip.post.adapter.out.persistence.repository; + +import konkuk.thip.post.application.port.out.dto.PostQueryDto; + +public interface PostQueryRepository { + PostQueryDto getPostQueryDtoByFeedId(Long feedId); + + PostQueryDto getPostQueryDtoByRecordId(Long recordId); + PostQueryDto getPostQueryDtoByVoteId(Long voteId); +} diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java new file mode 100644 index 000000000..12d4ce3fd --- /dev/null +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/repository/PostQueryRepositoryImpl.java @@ -0,0 +1,73 @@ +package konkuk.thip.post.adapter.out.persistence.repository; + +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.feed.adapter.out.jpa.QFeedJpaEntity; +import konkuk.thip.post.adapter.out.jpa.QPostJpaEntity; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; +import konkuk.thip.post.application.port.out.dto.QPostQueryDto; +import konkuk.thip.roompost.adapter.out.jpa.QRecordJpaEntity; +import konkuk.thip.roompost.adapter.out.jpa.QVoteJpaEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import static com.querydsl.jpa.JPAExpressions.treat; +import static konkuk.thip.post.domain.PostType.*; + +@Repository +@RequiredArgsConstructor +public class PostQueryRepositoryImpl implements PostQueryRepository { + + private final QPostJpaEntity post = QPostJpaEntity.postJpaEntity; + private final QFeedJpaEntity feed = QFeedJpaEntity.feedJpaEntity; + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public PostQueryDto getPostQueryDtoByFeedId(Long feedId) { + return jpaQueryFactory + .select(new QPostQueryDto( + feed.postId, + feed.userJpaEntity.userId, + feed.dtype, + Expressions.nullExpression(), + Expressions.nullExpression() + )) + .from(feed) + .where(feed.postId.eq(feedId)) + .where(feed.dtype.eq(FEED.getType())) + .fetchOne(); + } + + @Override + public PostQueryDto getPostQueryDtoByRecordId(Long recordId) { + return jpaQueryFactory + .select(new QPostQueryDto( + post.postId, + post.userJpaEntity.userId, + post.dtype, + treat(post, QRecordJpaEntity.class).page, // Record의 page + treat(post, QRecordJpaEntity.class).roomJpaEntity.roomId // Record의 roomId + )) + .from(post) + .where(post.postId.eq(recordId)) + .where(post.dtype.eq(RECORD.getType())) + .fetchOne(); + } + + @Override + public PostQueryDto getPostQueryDtoByVoteId(Long voteId) { + return jpaQueryFactory + .select(new QPostQueryDto( + post.postId, + post.userJpaEntity.userId, + post.dtype, + treat(post, QVoteJpaEntity.class).page, // Vote의 page + treat(post, QVoteJpaEntity.class).roomJpaEntity.roomId // Vote의 roomId + )) + .from(post) + .where(post.postId.eq(voteId)) + .where(post.dtype.eq(VOTE.getType())) + .fetchOne(); + } +} diff --git a/src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java b/src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java new file mode 100644 index 000000000..7682383d3 --- /dev/null +++ b/src/main/java/konkuk/thip/post/application/port/out/PostQueryPort.java @@ -0,0 +1,10 @@ +package konkuk.thip.post.application.port.out; + +import konkuk.thip.post.application.port.out.dto.PostQueryDto; + +public interface PostQueryPort { + PostQueryDto getPostQueryDtoByFeedId(Long feedId); + + PostQueryDto getPostQueryDtoByRecordId(Long recordId); + PostQueryDto getPostQueryDtoByVoteId(Long voteId); +} diff --git a/src/main/java/konkuk/thip/post/application/port/out/dto/PostQueryDto.java b/src/main/java/konkuk/thip/post/application/port/out/dto/PostQueryDto.java new file mode 100644 index 000000000..760a9679e --- /dev/null +++ b/src/main/java/konkuk/thip/post/application/port/out/dto/PostQueryDto.java @@ -0,0 +1,29 @@ +package konkuk.thip.post.application.port.out.dto; + +import com.querydsl.core.annotations.QueryProjection; +import jakarta.annotation.Nullable; + +public record PostQueryDto( + Long postId, + Long creatorId, + String postType, + @Nullable Integer page, + @Nullable Long roomId +) { + @QueryProjection + public PostQueryDto( + Long postId, + Long creatorId, + String postType, + @Nullable Integer page, + @Nullable Long roomId + ) { + this.postId = postId; + this.creatorId = creatorId; + this.postType = postType; + this.page = page; + this.roomId = roomId; + } + + +} diff --git a/src/main/java/konkuk/thip/post/application/service/PostLikeService.java b/src/main/java/konkuk/thip/post/application/service/PostLikeService.java index 21c0effcb..4ab27d818 100644 --- a/src/main/java/konkuk/thip/post/application/service/PostLikeService.java +++ b/src/main/java/konkuk/thip/post/application/service/PostLikeService.java @@ -1,5 +1,8 @@ package konkuk.thip.post.application.service; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; import konkuk.thip.post.application.service.handler.PostHandler; import konkuk.thip.post.domain.CountUpdatable; import konkuk.thip.post.application.port.in.dto.PostIsLikeCommand; @@ -8,7 +11,10 @@ import konkuk.thip.post.application.port.out.PostLikeCommandPort; import konkuk.thip.post.application.port.out.PostLikeQueryPort; import konkuk.thip.post.application.service.validator.PostLikeAuthorizationValidator; +import konkuk.thip.post.domain.PostType; import konkuk.thip.post.domain.service.PostCountService; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,11 +25,15 @@ public class PostLikeService implements PostLikeUseCase { private final PostLikeQueryPort postLikeQueryPort; private final PostLikeCommandPort postLikeCommandPort; + private final UserCommandPort userCommandPort; private final PostHandler postHandler; private final PostCountService postCountService; private final PostLikeAuthorizationValidator postLikeAuthorizationValidator; + private final FeedEventCommandPort feedEventCommandPort; + private final RoomEventCommandPort roomEventCommandPort; + @Override @Transactional public PostIsLikeResult changeLikeStatusPost(PostIsLikeCommand command) { @@ -41,6 +51,9 @@ public PostIsLikeResult changeLikeStatusPost(PostIsLikeCommand command) { if (command.isLike()) { postLikeAuthorizationValidator.validateUserCanLike(alreadyLiked); // μ’‹μ•„μš” κ°€λŠ₯ μ—¬λΆ€ 검증 postLikeCommandPort.save(command.userId(), command.postId(),command.postType()); + + // μ’‹μ•„μš” ν‘Έμ‰¬μ•Œλ¦Ό 전솑 + sendNotifications(command); } else { postLikeAuthorizationValidator.validateUserCanUnLike(alreadyLiked); // μ’‹μ•„μš” μ·¨μ†Œ κ°€λŠ₯ μ—¬λΆ€ 검증 postLikeCommandPort.delete(command.userId(), command.postId()); @@ -52,4 +65,19 @@ public PostIsLikeResult changeLikeStatusPost(PostIsLikeCommand command) { return PostIsLikeResult.of(post.getId(), command.isLike()); } + + private void sendNotifications(PostIsLikeCommand command) { + PostQueryDto postQueryDto = postHandler.getPostQueryDto(command.postType(), command.postId()); + + if(command.userId().equals(postQueryDto.creatorId())) return; // μžμ‹ μ˜ κ²Œμ‹œκΈ€μ— μ’‹μ•„μš” λˆ„λ₯΄λŠ” 경우 μ œμ™Έ + + User actorUser = userCommandPort.findById(command.userId()); + // μ’‹μ•„μš” ν‘Έμ‰¬μ•Œλ¦Ό 전솑 + if (command.postType() == PostType.FEED) { + feedEventCommandPort.publishFeedLikedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.postId()); + } + if (command.postType() == PostType.RECORD || command.postType() == PostType.VOTE) { + roomEventCommandPort.publishRoomPostLikedEvent(postQueryDto.creatorId(), actorUser.getId(), actorUser.getNickname(), postQueryDto.roomId(), postQueryDto.page(), postQueryDto.postId(), postQueryDto.postType()); + } + } } diff --git a/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java b/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java index a2bfaa4a8..df798c5df 100644 --- a/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java +++ b/src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java @@ -1,13 +1,15 @@ package konkuk.thip.post.application.service.handler; import konkuk.thip.common.annotation.application.HelperService; -import konkuk.thip.post.domain.CountUpdatable; -import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.application.port.out.FeedCommandPort; import konkuk.thip.feed.domain.Feed; +import konkuk.thip.post.application.port.out.PostQueryPort; +import konkuk.thip.post.application.port.out.dto.PostQueryDto; +import konkuk.thip.post.domain.CountUpdatable; +import konkuk.thip.post.domain.PostType; import konkuk.thip.roompost.application.port.out.RecordCommandPort; -import konkuk.thip.roompost.domain.Record; import konkuk.thip.roompost.application.port.out.VoteCommandPort; +import konkuk.thip.roompost.domain.Record; import konkuk.thip.roompost.domain.Vote; import lombok.RequiredArgsConstructor; @@ -19,6 +21,8 @@ public class PostHandler { private final RecordCommandPort recordCommandPort; private final VoteCommandPort voteCommandPort; + private final PostQueryPort postQueryPort; + public CountUpdatable findPost(PostType type, Long postId) { return switch (type) { case FEED -> feedCommandPort.getByIdOrThrow(postId); @@ -34,4 +38,12 @@ public void updatePost(PostType type, CountUpdatable post) { case VOTE -> voteCommandPort.updateVote((Vote) post); } } + + public PostQueryDto getPostQueryDto(PostType type, Long postId) { + return switch (type) { + case FEED -> postQueryPort.getPostQueryDtoByFeedId(postId); + case RECORD -> postQueryPort.getPostQueryDtoByRecordId(postId); + case VOTE -> postQueryPort.getPostQueryDtoByVoteId(postId); + }; + } } diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java index 4218bd247..8ac1aabe3 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java index 3f049e8ac..780dadb30 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchQueryPersistenceAdapter.java @@ -1,6 +1,6 @@ package konkuk.thip.recentSearch.adapter.out.persistence; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.adapter.out.mapper.RecentSearchMapper; import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.recentSearch.application.port.out.RecentSearchQueryPort; diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java index 309713668..29abb70e1 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepository.java @@ -1,7 +1,7 @@ package konkuk.thip.recentSearch.adapter.out.persistence.repository; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import java.util.List; import java.util.Optional; diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java index 7bd65aef4..5f5993118 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchQueryRepositoryImpl.java @@ -2,7 +2,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; diff --git a/src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java b/src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java index 04c09d662..4759f27ef 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java +++ b/src/main/java/konkuk/thip/recentSearch/application/port/out/RecentSearchQueryPort.java @@ -1,6 +1,6 @@ package konkuk.thip.recentSearch.application.port.out; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.domain.RecentSearch; import java.util.List; diff --git a/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java b/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java index 11e013211..d50c362cc 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java +++ b/src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java @@ -1,7 +1,7 @@ package konkuk.thip.recentSearch.application.service; import konkuk.thip.recentSearch.adapter.in.web.response.RecentSearchGetResponse; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.application.RecentSearchQueryMapper; import konkuk.thip.recentSearch.application.port.in.RecentSearchGetUseCase; import konkuk.thip.recentSearch.application.port.out.RecentSearchQueryPort; diff --git a/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java b/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java index 4de7be665..111e3db1a 100644 --- a/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java +++ b/src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java @@ -1,7 +1,7 @@ package konkuk.thip.recentSearch.application.service.manager; import konkuk.thip.common.annotation.application.HelperService; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; import konkuk.thip.recentSearch.application.port.out.RecentSearchQueryPort; import konkuk.thip.recentSearch.domain.RecentSearch; diff --git a/src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java b/src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java index 35d9a8ffc..38307eaea 100644 --- a/src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java +++ b/src/main/java/konkuk/thip/recentSearch/domain/RecentSearch.java @@ -1,7 +1,7 @@ package konkuk.thip.recentSearch.domain; import konkuk.thip.common.entity.BaseDomainEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchType.java b/src/main/java/konkuk/thip/recentSearch/domain/value/RecentSearchType.java similarity index 93% rename from src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchType.java rename to src/main/java/konkuk/thip/recentSearch/domain/value/RecentSearchType.java index 2a301c3ae..6f7340324 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchType.java +++ b/src/main/java/konkuk/thip/recentSearch/domain/value/RecentSearchType.java @@ -1,4 +1,4 @@ -package konkuk.thip.recentSearch.adapter.out.jpa; +package konkuk.thip.recentSearch.domain.value; import konkuk.thip.common.exception.InvalidStateException; diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index 35866f880..5865135fd 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -6,6 +6,7 @@ import konkuk.thip.common.entity.BaseJpaEntity; import konkuk.thip.room.domain.value.Category; import konkuk.thip.room.domain.Room; +import konkuk.thip.room.domain.value.RoomStatus; import lombok.*; import java.time.LocalDate; diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java index 3c75571c7..e54611c2e 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java @@ -4,6 +4,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; import konkuk.thip.room.domain.RoomParticipant; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.*; import org.hibernate.annotations.SQLDelete; diff --git a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomParticipantMapper.java b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomParticipantMapper.java index bf70e7863..69a1a433a 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomParticipantMapper.java +++ b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomParticipantMapper.java @@ -1,7 +1,7 @@ package konkuk.thip.room.adapter.out.mapper; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; import konkuk.thip.room.domain.RoomParticipant; diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java index 3a93b11f9..76c0053d7 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java @@ -11,9 +11,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; -import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_FOUND; +import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND; @Repository @RequiredArgsConstructor @@ -56,4 +58,22 @@ public void update(Room room) { roomJpaRepository.save(roomJpaEntity.updateFrom(room)); } + + @Override + public int updateRoomStateToExpired() { + return roomJpaRepository.updateRoomStatusToExpired(); + } + + @Override + public int updateRoomStateFromRecruitingToProgress() { + return roomJpaRepository.updateRoomStatusFromRecruitingToProgress(); + } + + @Override + public List findProgressTargetRooms() { + List roomJpaEntities = roomJpaRepository.findProgressTargetIds(); + return roomJpaEntities.stream() + .map(roomMapper::toDomainEntity) + .toList(); + } } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java index d43d06b5f..7f8de1638 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java @@ -71,6 +71,15 @@ public void update(RoomParticipant roomParticipant) { roomParticipantJpaRepository.save(roomParticipantJpaEntity); } + @Override + public RoomParticipant findHostByRoomId(Long roomId) { + RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findHostByRoomId(roomId).orElseThrow( + () -> new EntityNotFoundException(ErrorCode.ROOM_PARTICIPANT_NOT_FOUND) + ); + + return roomParticipantMapper.toDomainEntity(roomParticipantJpaEntity); + } + @Override public boolean existsHostUserInActiveRoom(Long userId) { return roomParticipantJpaRepository.existsHostUserInActiveRoom(userId); diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java index c58658c15..a41e7e811 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDate; +import java.util.List; import java.util.Optional; public interface RoomJpaRepository extends JpaRepository, RoomQueryRepository { @@ -29,4 +30,39 @@ public interface RoomJpaRepository extends JpaRepository, R where r.roomId = :roomId """) void updateRoomStats(@Param("roomId") Long roomId, @Param("avg") double avg, @Param("count") int count); + /** + * end_date < 였늘 => EXPIRED + * 이미 EXPIRED 인 것은 μ œμ™Έ + */ + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + update RoomJpaEntity r + set r.roomStatus = 'EXPIRED' + where r.endDate < current_date + and r.roomStatus <> 'EXPIRED' + """) + int updateRoomStatusToExpired(); + + /** + * start_date <= 였늘 AND end_date >= 였늘 => IN_PROGRESS + * RECRUITING 인 κ²ƒλ§Œ λŒ€μƒ + */ + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + update RoomJpaEntity r + set r.roomStatus = 'IN_PROGRESS' + where r.startDate <= current_date + and r.endDate >= current_date + and r.roomStatus = 'RECRUITING' + """) + int updateRoomStatusFromRecruitingToProgress(); + + @Query(""" + select r + from RoomJpaEntity r + where r.startDate <= current_date + and r.endDate >= current_date + and r.roomStatus = 'RECRUITING' + """) + List findProgressTargetIds(); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index ef8120a16..9a2adb513 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java @@ -32,6 +32,11 @@ public interface RoomParticipantJpaRepository extends JpaRepository findHostByRoomId(@Param("roomId") Long roomId); + @Query("SELECT CASE WHEN COUNT(rp) > 0 THEN true ELSE false END " + "FROM RoomParticipantJpaEntity rp " + "JOIN RoomJpaEntity r ON rp.roomJpaEntity.roomId = r.roomId " + diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomStateChangeUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomStateChangeUseCase.java new file mode 100644 index 000000000..b826f2096 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomStateChangeUseCase.java @@ -0,0 +1,6 @@ +package konkuk.thip.room.application.port.in; + +public interface RoomStateChangeUseCase { + void changeRoomStateToExpired(); + void changeRoomStateToProgress(); +} diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java index bbc98a6a9..cfa305590 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java @@ -3,6 +3,7 @@ import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.room.domain.Room; +import java.util.List; import java.util.Optional; import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND; @@ -19,4 +20,10 @@ default Room getByIdOrThrow(Long id) { Long save(Room room); void update(Room room); + + int updateRoomStateToExpired(); + + int updateRoomStateFromRecruitingToProgress(); + + List findProgressTargetRooms(); } diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java index 81d93696f..e46b52c01 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java @@ -25,6 +25,8 @@ default RoomParticipant getByUserIdAndRoomIdOrThrow(Long userId, Long roomId) { void update(RoomParticipant roomParticipant); + RoomParticipant findHostByRoomId(Long roomId); + boolean existsHostUserInActiveRoom(Long userId); void deleteAllByUserId(Long userId); diff --git a/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java b/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java index 619a3c43d..3f4167821 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomJoinService.java @@ -2,6 +2,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.room.application.port.in.RoomJoinUseCase; import konkuk.thip.room.application.port.in.dto.RoomJoinCommand; import konkuk.thip.room.application.port.in.dto.RoomJoinResult; @@ -10,6 +11,8 @@ import konkuk.thip.room.domain.Room; import konkuk.thip.room.application.port.in.dto.RoomJoinType; import konkuk.thip.room.domain.RoomParticipant; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +25,9 @@ public class RoomJoinService implements RoomJoinUseCase { private final RoomCommandPort roomCommandPort; private final RoomParticipantCommandPort roomParticipantCommandPort; + private final UserCommandPort userCommandPort; + + private final RoomEventCommandPort roomEventCommandPort; @Override @Transactional @@ -45,9 +51,20 @@ public RoomJoinResult changeJoinState(RoomJoinCommand roomJoinCommand) { // 방의 μƒνƒœ μ—…λ°μ΄νŠΈ roomCommandPort.update(room); + // μ°Έμ—¬μž 푸쉬 μ•Œλ¦Ό 전솑 (ν˜ΈμŠ€νŠΈμ—κ²Œλ§Œ 전솑) + if (type == RoomJoinType.JOIN) { + sendNotifications(roomJoinCommand, room); + } + return RoomJoinResult.of(room.getId(), type.getType()); } + private void sendNotifications(RoomJoinCommand roomJoinCommand, Room room) { + RoomParticipant targetUser = roomParticipantCommandPort.findHostByRoomId(room.getId()); + User actorUser = userCommandPort.findById(roomJoinCommand.userId()); + roomEventCommandPort.publishRoomJoinEventToHost(targetUser.getUserId(), room.getId(), room.getTitle(), actorUser.getId(), actorUser.getNickname()); + } + private void handleCancel(RoomJoinCommand roomJoinCommand, Optional participantOptional, Optional roomParticipantOptional, Room room) { // μ°Έμ—¬ν•˜μ§€ μ•Šμ€ μƒνƒœ RoomParticipant participant = participantOptional.orElseThrow(() -> diff --git a/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java b/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java index e8c37d33b..0b8140a9a 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomRecruitCloseService.java @@ -2,6 +2,7 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.room.application.port.in.RoomRecruitCloseUseCase; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; @@ -11,6 +12,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor public class RoomRecruitCloseService implements RoomRecruitCloseUseCase { @@ -18,7 +21,8 @@ public class RoomRecruitCloseService implements RoomRecruitCloseUseCase { private final RoomParticipantCommandPort roomParticipantCommandPort; private final RoomCommandPort roomCommandPort; - //todo λͺ¨μ§‘ λ§ˆκ°μ‹œ λ°© μ°Έμ—¬μžλ“€μ—κ²Œ λͺ¨μ§‘ 마감 μ•Œλ¦Ό 전솑 + private final RoomEventCommandPort roomEventCommandPort; + @Override @Transactional public Long closeRoomRecruit(Long userId, Long roomId) { @@ -37,9 +41,19 @@ public Long closeRoomRecruit(Long userId, Long roomId) { // 4. Room ν…Œμ΄λΈ” μ—…λ°μ΄νŠΈ roomCommandPort.update(room); + // 5. λ°© μ°Έμ—¬μžλ“€μ—κ²Œ λͺ¨μ§‘ 마감 μ•Œλ¦Ό 전솑 (호슀트 μ œμ™Έ) + sendNotifications(roomId, room); + return room.getId(); } + private void sendNotifications(Long roomId, Room room) { + List actorUsers = roomParticipantCommandPort.findAllByRoomId(roomId); + for (RoomParticipant participant : actorUsers) { + if(participant.isHost()) continue; // ν˜ΈμŠ€νŠΈλŠ” μ œμ™Έ + roomEventCommandPort.publishRoomRecruitClosedEarlyEvent(participant.getUserId(), roomId, room.getTitle()); + } + } private void validateCloseable(RoomParticipant roomParticipant) { if (roomParticipant.isMember()) { diff --git a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java index e3a0d3091..849a38051 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomSearchService.java @@ -2,7 +2,7 @@ import konkuk.thip.common.util.Cursor; import konkuk.thip.common.util.CursorBasedList; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.application.service.manager.RecentSearchCreateManager; import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; import konkuk.thip.room.application.mapper.RoomQueryMapper; diff --git a/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java b/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java new file mode 100644 index 000000000..7bb9604ff --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/RoomStateChangeService.java @@ -0,0 +1,61 @@ +package konkuk.thip.room.application.service; + +import konkuk.thip.message.application.port.out.RoomEventCommandPort; +import konkuk.thip.room.application.port.in.RoomStateChangeUseCase; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; +import konkuk.thip.room.domain.Room; +import konkuk.thip.room.domain.RoomParticipant; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RoomStateChangeService implements RoomStateChangeUseCase { + + private final RoomCommandPort roomCommandPort; + private final RoomParticipantCommandPort roomParticipantCommandPort; + + private final RoomEventCommandPort roomEventCommandPort; + + /** + * end_date < 였늘 => EXPIRED + */ + @Async("schedulerAsyncExecutor") + @Override + @Transactional + public void changeRoomStateToExpired() { + int updated = roomCommandPort.updateRoomStateToExpired(); + log.info("[RoomState] EXPIRED둜 λ³€κ²½λœ 건수={}", updated); + } + + /** + * start_date <= 였늘 AND end_date >= 였늘 => IN_PROGRESS + */ + @Async("schedulerAsyncExecutor") + @Override + @Transactional + public void changeRoomStateToProgress() { + // λ°© λͺ¨μž„λ°© ν™œλ™ μ‹œμž‘ ν‘Έμ‰¬μ•Œλ¦Ό 전솑 + sendNotifications(); + + int updated = roomCommandPort.updateRoomStateFromRecruitingToProgress(); + log.info("[RoomState] IN_PROGRESS둜 λ³€κ²½λœ 건수={}", updated); + } + + private void sendNotifications() { + List targetRooms = roomCommandPort.findProgressTargetRooms(); + for (Room room : targetRooms) { + List targetUsers = roomParticipantCommandPort.findAllByRoomId(room.getId()); + for (RoomParticipant participant : targetUsers) { + roomEventCommandPort.publishRoomActivityStartedEvent(participant.getUserId(), room.getId(), room.getTitle()); + } + } + } +} diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index 010198b5b..ad992f0cd 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -4,7 +4,7 @@ import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.entity.StatusType; import konkuk.thip.common.exception.code.ErrorCode; -import konkuk.thip.room.adapter.out.jpa.RoomStatus; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.room.domain.value.Category; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/src/main/java/konkuk/thip/room/domain/RoomParticipant.java b/src/main/java/konkuk/thip/room/domain/RoomParticipant.java index ca9c3b081..f76bf3f7d 100644 --- a/src/main/java/konkuk/thip/room/domain/RoomParticipant.java +++ b/src/main/java/konkuk/thip/room/domain/RoomParticipant.java @@ -2,7 +2,7 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/src/main/java/konkuk/thip/room/domain/RoomParticipants.java b/src/main/java/konkuk/thip/room/domain/RoomParticipants.java index cf247162f..bf3f58ab9 100644 --- a/src/main/java/konkuk/thip/room/domain/RoomParticipants.java +++ b/src/main/java/konkuk/thip/room/domain/RoomParticipants.java @@ -1,7 +1,7 @@ package konkuk.thip.room.domain; import konkuk.thip.common.exception.InvalidStateException; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantRole.java b/src/main/java/konkuk/thip/room/domain/value/RoomParticipantRole.java similarity index 94% rename from src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantRole.java rename to src/main/java/konkuk/thip/room/domain/value/RoomParticipantRole.java index 9c818af65..42590a01a 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantRole.java +++ b/src/main/java/konkuk/thip/room/domain/value/RoomParticipantRole.java @@ -1,5 +1,5 @@ -package konkuk.thip.room.adapter.out.jpa; +package konkuk.thip.room.domain.value; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java b/src/main/java/konkuk/thip/room/domain/value/RoomStatus.java similarity index 63% rename from src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java rename to src/main/java/konkuk/thip/room/domain/value/RoomStatus.java index b1952c30f..50fe90d32 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java +++ b/src/main/java/konkuk/thip/room/domain/value/RoomStatus.java @@ -1,4 +1,4 @@ -package konkuk.thip.room.adapter.out.jpa; +package konkuk.thip.room.domain.value; public enum RoomStatus { RECRUITING, diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java index 840f0d8ac..8d6351fb3 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java @@ -3,14 +3,14 @@ import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.mapper.RecordMapper; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.roompost.application.port.out.RecordCommandPort; import konkuk.thip.roompost.domain.Record; -import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java index 8db4e64ae..b74daa966 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java @@ -3,11 +3,9 @@ import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; -import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.roompost.adapter.out.jpa.VoteItemJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.VoteParticipantJpaEntity; @@ -21,6 +19,8 @@ import konkuk.thip.roompost.domain.Vote; import konkuk.thip.roompost.domain.VoteItem; import konkuk.thip.roompost.domain.VoteParticipant; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; diff --git a/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java b/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java index e3224f6f6..7e075f9cf 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordCreateService.java @@ -3,6 +3,7 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.roompost.application.port.in.RecordCreateUseCase; import konkuk.thip.roompost.application.port.in.dto.record.RecordCreateCommand; import konkuk.thip.roompost.application.port.in.dto.record.RecordCreateResult; @@ -14,10 +15,14 @@ import konkuk.thip.room.domain.Room; import konkuk.thip.room.domain.RoomParticipant; import konkuk.thip.roompost.application.service.manager.RoomProgressManager; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + import static konkuk.thip.common.exception.code.ErrorCode.RECORD_CANNOT_BE_OVERVIEW; @Service @@ -28,10 +33,13 @@ public class RecordCreateService implements RecordCreateUseCase { private final RoomCommandPort roomCommandPort; private final BookCommandPort bookCommandPort; private final RoomParticipantCommandPort roomParticipantCommandPort; + private final UserCommandPort userCommandPort; private final RoomParticipantValidator roomParticipantValidator; private final RoomProgressManager roomProgressManager; + private final RoomEventCommandPort roomEventCommandPort; + @Override @Transactional //todo updateRoomPercentage μŠ€μΌ€μ€„λŸ¬λ‘œ μ±…μž„μ„ 뢄리할지 λ…Όμ˜ @@ -63,9 +71,21 @@ public RecordCreateResult createRecord(RecordCreateCommand command) { // 5. RoomParticipant, Room progress 정보 update roomProgressManager.updateUserAndRoomProgress(roomParticipant, room, book, record.getPage()); + // 6. λͺ¨μž„λ°© μ°Έμ—¬μžλ“€μ—κ²Œ 기둝 생성 μ•Œλ¦Ό 전솑 (본인 μ œμ™Έ) + sendNotifications(command, room, record, newRecordId); + return RecordCreateResult.of(newRecordId, command.roomId()); } + private void sendNotifications(RecordCreateCommand command, Room room, Record record, Long newRecordId) { + User actorUser = userCommandPort.findById(command.userId()); + List targetUsers = roomParticipantCommandPort.findAllByRoomId(command.roomId()); + for (RoomParticipant targetUser : targetUsers) { + if (targetUser.getUserId().equals(command.userId())) continue; // 본인 μ œμ™Έ + roomEventCommandPort.publishRoomRecordCreatedEvent(targetUser.getUserId(), actorUser.getId(), actorUser.getNickname(), room.getId(), room.getTitle(), record.getPage(), newRecordId); + } + } + private void validateRoomParticipant(RoomParticipant roomParticipant, boolean isOverview) { // UserRoom의 총평 μž‘μ„± κ°€λŠ₯ μ—¬λΆ€ 검증 if (!roomParticipant.canWriteOverview() && isOverview) { diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java index f7098d6be..ce7cf3788 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteCreateService.java @@ -2,16 +2,17 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; import konkuk.thip.room.application.service.validator.RoomParticipantValidator; import konkuk.thip.room.domain.Room; import konkuk.thip.room.domain.RoomParticipant; -import konkuk.thip.roompost.application.service.manager.RoomProgressManager; import konkuk.thip.roompost.application.port.in.VoteCreateUseCase; import konkuk.thip.roompost.application.port.in.dto.vote.VoteCreateCommand; import konkuk.thip.roompost.application.port.in.dto.vote.VoteCreateResult; import konkuk.thip.roompost.application.port.out.VoteCommandPort; +import konkuk.thip.roompost.application.service.manager.RoomProgressManager; import konkuk.thip.roompost.domain.Vote; import konkuk.thip.roompost.domain.VoteItem; import lombok.RequiredArgsConstructor; @@ -34,6 +35,8 @@ public class VoteCreateService implements VoteCreateUseCase { private final RoomProgressManager roomProgressManager; + private final RoomEventCommandPort roomEventCommandPort; + @Transactional @Override public VoteCreateResult createVote(VoteCreateCommand command) { @@ -68,9 +71,20 @@ public VoteCreateResult createVote(VoteCreateCommand command) { // 4. RoomParticipant, Room progress 정보 update roomProgressManager.updateUserAndRoomProgress(roomParticipant, room, book, vote.getPage()); + // 5. λͺ¨μž„λ°© μ°Έμ—¬μžλ“€μ—κ²Œ νˆ¬ν‘œ 생성 μ•Œλ¦Ό 전솑 (본인 μ œμ™Έ) + sendNotifications(command, room, vote, savedVoteId); + return VoteCreateResult.of(savedVoteId, command.roomId()); } + private void sendNotifications(VoteCreateCommand command, Room room, Vote vote, Long newVoteId) { + List targetUsers = roomParticipantCommandPort.findAllByRoomId(command.roomId()); + for (RoomParticipant targetUser : targetUsers) { + if (targetUser.getUserId().equals(command.userId())) continue; // 본인 μ œμ™Έ + roomEventCommandPort.publishRoomVoteStartedEvent(targetUser.getUserId(), room.getId(), room.getTitle(), vote.getPage(), newVoteId); + } + } + private void validateVote(Vote vote, Book book) { // νŽ˜μ΄μ§€ μœ νš¨μ„± 검증 vote.validatePage(book.getPageCount()); diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java index 603a12913..5fbb26544 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java @@ -6,6 +6,7 @@ import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.user.domain.value.Alias; import konkuk.thip.user.domain.User; +import konkuk.thip.user.domain.value.UserRole; import lombok.*; import org.hibernate.annotations.SQLDelete; diff --git a/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java b/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java index 0eaeacf67..bb479a7b3 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java +++ b/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java @@ -1,7 +1,7 @@ package konkuk.thip.user.adapter.out.mapper; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.domain.User; import org.springframework.stereotype.Component; diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java index b8d9a774e..bff694526 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java @@ -3,10 +3,10 @@ import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.common.util.DateUtil; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.application.port.out.dto.FollowingQueryDto; -import konkuk.thip.user.application.port.out.dto.UserQueryDto; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.application.port.out.FollowingQueryPort; +import konkuk.thip.user.application.port.out.dto.FollowingQueryDto; +import konkuk.thip.user.application.port.out.dto.UserQueryDto; import konkuk.thip.user.domain.value.Alias; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java index d95bce510..8e4535ece 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java @@ -2,11 +2,13 @@ import konkuk.thip.common.util.Cursor; import konkuk.thip.common.util.CursorBasedList; +import konkuk.thip.user.adapter.out.mapper.UserMapper; import konkuk.thip.user.adapter.out.persistence.function.ReactionQueryFunction; import konkuk.thip.user.application.port.out.dto.ReactionQueryDto; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.application.port.out.UserQueryPort; import konkuk.thip.user.application.port.out.dto.UserQueryDto; +import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -19,6 +21,7 @@ public class UserQueryPersistenceAdapter implements UserQueryPort { private final UserJpaRepository userJpaRepository; + private final UserMapper userMapper; @Override public boolean existsByNickname(String nickname) { @@ -66,6 +69,13 @@ public CursorBasedList findBothReactionsByUserId(Long userId, ); } + @Override + public List getAllFollowersByUserId(Long userId) { + return userJpaRepository.findAllFollowersByUserId(userId).stream() + .map(userMapper::toDomainEntity) + .toList(); + } + private CursorBasedList getReactions(Long userId, Cursor cursor, ReactionQueryFunction reactionQueryFunction) { LocalDateTime cursorLocalDateTime = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0); diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java index 497296710..166f3795d 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java @@ -25,4 +25,7 @@ public interface UserJpaRepository extends JpaRepository, U @Query("SELECT f.followingUserJpaEntity.userId FROM FollowingJpaEntity f WHERE f.userJpaEntity.userId = :userId") List findAllTargetUserIdsByUserId(@Param("userId") Long userId); + + @Query("SELECT f.userJpaEntity FROM FollowingJpaEntity f WHERE f.followingUserJpaEntity.userId = :userId") + List findAllFollowersByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java index 29be62336..3e6de1cdd 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java @@ -3,8 +3,8 @@ import konkuk.thip.common.util.Cursor; import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.user.application.port.out.dto.ReactionQueryDto; -import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; import konkuk.thip.user.application.port.out.dto.UserQueryDto; +import konkuk.thip.user.domain.User; import java.util.List; import java.util.Set; @@ -25,4 +25,6 @@ public interface UserQueryPort { CursorBasedList findCommentReactionsByUserId(Long userId, Cursor cursor, String label); CursorBasedList findBothReactionsByUserId(Long userId, Cursor cursor, String likeLabel, String commentLabel); + + List getAllFollowersByUserId(Long userId); } diff --git a/src/main/java/konkuk/thip/user/application/service/UserSearchService.java b/src/main/java/konkuk/thip/user/application/service/UserSearchService.java index e9c9a6267..098f428b3 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserSearchService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserSearchService.java @@ -1,6 +1,6 @@ package konkuk.thip.user.application.service; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.application.service.manager.RecentSearchCreateManager; import konkuk.thip.user.adapter.in.web.response.UserSearchResponse; import konkuk.thip.user.application.mapper.UserQueryMapper; diff --git a/src/main/java/konkuk/thip/user/application/service/UserSignupService.java b/src/main/java/konkuk/thip/user/application/service/UserSignupService.java index 04eb3323b..716a8c297 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserSignupService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserSignupService.java @@ -14,7 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static konkuk.thip.user.adapter.out.jpa.UserRole.USER; +import static konkuk.thip.user.domain.value.UserRole.USER; @Service diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java b/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java index d47406f49..24e9eeb4c 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserFollowService.java @@ -1,6 +1,7 @@ package konkuk.thip.user.application.service.following; import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; import konkuk.thip.user.application.port.in.UserFollowUsecase; import konkuk.thip.user.application.port.in.dto.UserFollowCommand; import konkuk.thip.user.application.port.out.FollowingCommandPort; @@ -22,6 +23,8 @@ public class UserFollowService implements UserFollowUsecase { private final FollowingCommandPort followingCommandPort; private final UserCommandPort userCommandPort; + private final FeedEventCommandPort feedEventCommandPort; + @Override @Transactional public Boolean changeFollowingState(UserFollowCommand followCommand) { @@ -39,6 +42,9 @@ public Boolean changeFollowingState(UserFollowCommand followCommand) { if (isFollowRequest) { // νŒ”λ‘œμš° μš”μ²­μΈ 경우 targetUser.increaseFollowerCount(); followingCommandPort.save(Following.withoutId(userId, targetUserId), targetUser); + + // νŒ”λ‘œμš° ν‘Έμ‰¬μ•Œλ¦Ό 전솑 + sendNotifications(userId, targetUserId); return true; } else { // μ–ΈνŒ”λ‘œμš° μš”μ²­μΈ 경우 targetUser.decreaseFollowerCount(); @@ -47,6 +53,11 @@ public Boolean changeFollowingState(UserFollowCommand followCommand) { } } + private void sendNotifications(Long userId, Long targetUserId) { + User actorUser = userCommandPort.findById(userId); + feedEventCommandPort.publishFollowEvent(targetUserId, actorUser.getId(), actorUser.getNickname()); + } + private void validateParams(Long userId, Long targetUserId) { if(userId.equals(targetUserId)) { throw new BusinessException(USER_CANNOT_FOLLOW_SELF); diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserRole.java b/src/main/java/konkuk/thip/user/domain/value/UserRole.java similarity index 93% rename from src/main/java/konkuk/thip/user/adapter/out/jpa/UserRole.java rename to src/main/java/konkuk/thip/user/domain/value/UserRole.java index 7bf0dc750..964394bfa 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserRole.java +++ b/src/main/java/konkuk/thip/user/domain/value/UserRole.java @@ -1,5 +1,5 @@ -package konkuk.thip.user.adapter.out.jpa; +package konkuk.thip.user.domain.value; import lombok.Getter; diff --git a/src/main/resources/db/migration/V250828__Add_room_status.sql b/src/main/resources/db/migration/V250828__Add_room_status.sql index 0110d59de..23a57aedd 100644 --- a/src/main/resources/db/migration/V250828__Add_room_status.sql +++ b/src/main/resources/db/migration/V250828__Add_room_status.sql @@ -11,4 +11,4 @@ WHERE start_date <= CURDATE(); -- 3) end_dateκ°€ ν˜„μž¬ λ‚ μ§œλ³΄λ‹€ "이전"이면 EXPIRED둜 μ—…λ°μ΄νŠΈ UPDATE rooms SET room_status = 'EXPIRED' -WHERE end_date < CURDATE(); \ No newline at end of file +WHERE end_date < CURDATE(); diff --git a/src/main/resources/db/migration/V250904__Create_FcmToken_table.sql b/src/main/resources/db/migration/V250904__Create_FcmToken_table.sql new file mode 100644 index 000000000..22d491380 --- /dev/null +++ b/src/main/resources/db/migration/V250904__Create_FcmToken_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE fcm_tokens ( + fcm_token_id BIGINT AUTO_INCREMENT PRIMARY KEY, + fcm_token VARCHAR(255) NOT NULL, + device_id VARCHAR(128) NOT NULL UNIQUE, + platform VARCHAR(16) NOT NULL, + last_used_time DATE NOT NULL, + is_enabled BOOLEAN NOT NULL, + user_id BIGINT NOT NULL, + created_at DATETIME(6) NOT NULL, + modified_at DATETIME(6) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE', + CONSTRAINT fk_fcm_tokens_user + FOREIGN KEY (user_id) REFERENCES users(user_id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java index 543e136d2..fe5aa6855 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java @@ -10,7 +10,7 @@ import konkuk.thip.book.adapter.out.jpa.SavedBookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java index f3d07d72b..0da7110b4 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java @@ -8,14 +8,14 @@ import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.book.adapter.out.jpa.SavedBookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java index 928891ec7..030332ce1 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java @@ -7,7 +7,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java index 13e416567..a1cec3e9d 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java index adfbc9bd2..93e0f5145 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookRecruitingRoomApiTest.java @@ -9,7 +9,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java index d13fd4130..a2f307559 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java @@ -4,10 +4,10 @@ import konkuk.thip.common.security.util.JwtUtil; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateApiTest.java index 7d0d26958..379e1c787 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateApiTest.java @@ -8,7 +8,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java index 4c1640329..7f9346f20 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java @@ -11,7 +11,7 @@ import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java index b39a96877..e0bdbd54a 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java @@ -12,7 +12,7 @@ import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 003c3ebdc..27fb9b62a 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -13,15 +13,15 @@ import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; import konkuk.thip.post.domain.PostType; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.domain.value.Category; import konkuk.thip.roompost.adapter.out.jpa.*; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.domain.value.Alias; import java.time.LocalDate; diff --git a/src/test/java/konkuk/thip/config/TestAsyncConfig.java b/src/test/java/konkuk/thip/config/TestAsyncConfig.java index 398f62fb9..e6b2d97cd 100644 --- a/src/test/java/konkuk/thip/config/TestAsyncConfig.java +++ b/src/test/java/konkuk/thip/config/TestAsyncConfig.java @@ -4,15 +4,29 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; import java.util.concurrent.Executor; +// ν…ŒμŠ€νŠΈμš©: 동기 μ‹€ν–‰ κ°•μ œ +@EnableAsync @Configuration @Profile("test") -public class TestAsyncConfig { - @Bean(name = "taskExecutor") - public Executor taskExecutor() { - return new SyncTaskExecutor(); // 동기 μ‹€ν–‰ +public class TestAsyncConfig implements AsyncConfigurer { + + @Bean(name = "fcmAsyncExecutor") + public Executor fcmAsyncExecutor() { + return new SyncTaskExecutor(); + } + + @Bean(name = "schedulerAsyncExecutor") + public Executor schedulerAsyncExecutor() { + return new SyncTaskExecutor(); } -} + @Override + public Executor getAsyncExecutor() { + return new SyncTaskExecutor(); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java index bf8dfb4a8..cd6c59e9c 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.feed.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java index 921c22204..ca31c8511 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java @@ -7,7 +7,7 @@ import konkuk.thip.feed.adapter.in.web.request.FeedIsLikeRequest; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java index 8b39285be..0d4627b3d 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java @@ -9,7 +9,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedRelatedWithBookApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedRelatedWithBookApiTest.java index 66095fe48..9ee2dfab1 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedRelatedWithBookApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedRelatedWithBookApiTest.java @@ -9,10 +9,10 @@ import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java index 2b5fb1b69..7636df682 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSavedListApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSavedListApiTest.java index 4d3ff8597..a0dd6403c 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSavedListApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSavedListApiTest.java @@ -8,7 +8,7 @@ import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java index 244c43b52..450798708 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java @@ -8,7 +8,7 @@ import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.feed.domain.value.Tag; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java index 59237330f..6701bfb82 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.feed.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java index 62025dad9..4c4e1b3fb 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.feed.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java b/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java new file mode 100644 index 000000000..bbd7ee37e --- /dev/null +++ b/src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java @@ -0,0 +1,48 @@ +// src/test/java/konkuk/thip/message/adapter/in/event/MessageFeedEventListenerTest.java +package konkuk.thip.message.adapter.in.event; + +import konkuk.thip.message.adapter.out.event.dto.FeedEvents; +import konkuk.thip.message.application.port.in.FeedNotificationDispatchUseCase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.Transactional; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("[λ‹¨μœ„] MessageRoomEventListener λ‹¨μœ„ ν…ŒμŠ€νŠΈ") +class MessageFeedEventListenerTest { + + @Autowired + private org.springframework.context.ApplicationEventPublisher publisher; + + @MockitoBean + private FeedNotificationDispatchUseCase feedUseCase; + + @Test + @Transactional + @DisplayName("FollowerEvent λ°œν–‰ β†’ 컀밋 μ‹œ 이벀트 λ¦¬μŠ€λ„ˆκ°€ useCase.handleFollower 호좜") + void follower_isHandled_afterCommit() { + // given + var e = FeedEvents.FollowerEvent.builder() + .targetUserId(1L).actorUserId(2L).actorUsername("bob").build(); + + // when: νŠΈλžœμž­μ…˜ μ•ˆμ—μ„œ 이벀트 λ°œν–‰ + publisher.publishEvent(e); + + // THEN: AFTER_COMMIT μ‹œμ  λ§Œλ“€μ–΄μ£ΌκΈ° + TestTransaction.flagForCommit(); + TestTransaction.end(); // μ—¬κΈ°μ„œ @TransactionalEventListener(AFTER_COMMIT) μ‹€ν–‰ + + // then + verify(feedUseCase, times(1)).handleFollower(Mockito.eq(e)); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java b/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java new file mode 100644 index 000000000..25d89a434 --- /dev/null +++ b/src/test/java/konkuk/thip/message/adapter/in/event/MessageRoomEventListenerTest.java @@ -0,0 +1,45 @@ +package konkuk.thip.message.adapter.in.event; + +import konkuk.thip.message.adapter.out.event.dto.RoomEvents; +import konkuk.thip.message.application.port.in.RoomNotificationDispatchUseCase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.Transactional; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("[λ‹¨μœ„] MessageRoomEventListener λ‹¨μœ„ ν…ŒμŠ€νŠΈ") +class MessageRoomEventListenerTest { + + @Autowired + private org.springframework.context.ApplicationEventPublisher publisher; + + @MockitoBean + private RoomNotificationDispatchUseCase roomUseCase; + + @Test + @Transactional + @DisplayName("RoomPostCommentedEvent λ°œν–‰ β†’ 컀밋 μ‹œ 이벀트 λ¦¬μŠ€λ„ˆκ°€ useCase.handleRoomPostCommented 호좜") + void roomPostCommented_isHandled_afterCommit() { + var e = RoomEvents.RoomPostCommentedEvent.builder() + .targetUserId(10L).actorUserId(20L).actorUsername("alice") + .roomId(100L).page(12).postId(999L).postType("RECORD") + .build(); + + publisher.publishEvent(e); + + TestTransaction.flagForCommit(); + TestTransaction.end(); + + verify(roomUseCase, times(1)).handleRoomPostCommented(Mockito.eq(e)); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchDeleteApiTest.java b/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchDeleteApiTest.java index 6331a31d4..f1a875e3b 100644 --- a/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchDeleteApiTest.java +++ b/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchDeleteApiTest.java @@ -3,7 +3,7 @@ import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; diff --git a/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchGetApiTest.java b/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchGetApiTest.java index 64b351594..31075637d 100644 --- a/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchGetApiTest.java +++ b/src/test/java/konkuk/thip/recentSearch/adapter/in/web/RecentSearchGetApiTest.java @@ -3,7 +3,7 @@ import jakarta.persistence.EntityManager; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java index c69cda6e9..2a399c451 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCloseJoinApiTest.java @@ -5,12 +5,12 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java index f732cc077..829039b1b 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java @@ -11,7 +11,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java index 124a674d9..c3e1e72ad 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.common.util.DateUtil; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java index 658146dfa..1e30aa466 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.*; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java index dd2ceb508..480bba84d 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java @@ -4,12 +4,12 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java index 8b9164db5..f8e187a2e 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java @@ -6,12 +6,12 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java index 6210bb75c..c61fa931d 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java @@ -7,12 +7,12 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java index 1a60e42b4..90f6e4a24 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java @@ -6,12 +6,12 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.roompost.adapter.out.jpa.VoteItemJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java index f41349c8e..9d2362577 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java @@ -6,13 +6,13 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.in.web.request.RoomPostIsLikeRequest; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusControllerTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusControllerTest.java index f0057b227..e8b21e592 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusControllerTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusControllerTest.java @@ -8,7 +8,7 @@ import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java index cbad6f571..0e11daa9b 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java @@ -6,13 +6,14 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.adapter.out.jpa.*; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.domain.value.Alias; +import konkuk.thip.user.domain.value.UserRole; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java index 907335375..2493e5ce5 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; @@ -24,7 +24,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import java.sql.Timestamp; import java.time.LocalDate; import static konkuk.thip.common.exception.code.ErrorCode.INVALID_MY_ROOM_TYPE; diff --git a/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java b/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java index c94bedfe6..383d0933d 100644 --- a/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java +++ b/src/test/java/konkuk/thip/room/application/service/RoomJoinServiceTest.java @@ -2,17 +2,22 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.message.application.port.out.RoomEventCommandPort; import konkuk.thip.room.application.port.in.dto.RoomJoinCommand; import konkuk.thip.room.application.port.out.RoomCommandPort; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; import konkuk.thip.room.domain.Room; import konkuk.thip.room.domain.RoomParticipant; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; +import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import static konkuk.thip.room.application.port.in.dto.RoomJoinType.CANCEL; @@ -26,6 +31,8 @@ class RoomJoinServiceTest { private RoomCommandPort roomCommandPort; private RoomParticipantCommandPort roomParticipantCommandPort; private RoomJoinService roomJoinService; + private UserCommandPort userCommandPort; + private RoomEventCommandPort roomEventCommandPort; private final Long ROOM_ID = 1L; private final Long USER_ID = 2L; @@ -38,10 +45,14 @@ class RoomJoinServiceTest { void setUp() { roomCommandPort = mock(RoomCommandPort.class); roomParticipantCommandPort = mock(RoomParticipantCommandPort.class); + userCommandPort = mock(UserCommandPort.class); + roomEventCommandPort = mock(RoomEventCommandPort.class); roomJoinService = new RoomJoinService( roomCommandPort, - roomParticipantCommandPort + roomParticipantCommandPort, + userCommandPort, + roomEventCommandPort ); } @@ -71,6 +82,17 @@ void successJoin() { given(roomCommandPort.findById(ROOM_ID)).willReturn(Optional.of(room)); given(roomParticipantCommandPort.findByUserIdAndRoomIdOptional(USER_ID, ROOM_ID)) .willReturn(Optional.empty()); + given(roomParticipantCommandPort.findHostByRoomId(any())) + .willReturn(RoomParticipant.hostWithoutId(100L, ROOM_ID)); + given(roomParticipantCommandPort.findAllByRoomId(any())).willReturn( + List.of( + RoomParticipant.memberWithoutId(3L, ROOM_ID), + RoomParticipant.memberWithoutId(4L, ROOM_ID) + ) + ); + given(userCommandPort.findById(any())).willReturn( + User.withoutId("nickname", "μΌλ°˜μœ μ €", "kakao_1234", Alias.WRITER) + ); roomJoinService.changeJoinState(command); diff --git a/src/test/java/konkuk/thip/room/domain/RoomParticipantsTest.java b/src/test/java/konkuk/thip/room/domain/RoomParticipantsTest.java index a935d5a94..7c1361a34 100644 --- a/src/test/java/konkuk/thip/room/domain/RoomParticipantsTest.java +++ b/src/test/java/konkuk/thip/room/domain/RoomParticipantsTest.java @@ -1,7 +1,7 @@ package konkuk.thip.room.domain; import konkuk.thip.common.exception.InvalidStateException; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java index 99493b7fd..a3117747d 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java @@ -8,7 +8,7 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java index 19ee0b192..d9274100a 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java index ed944bdd0..949369b07 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java index 23e92f679..dd779b8e0 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordCreateControllerTest.java @@ -11,7 +11,7 @@ import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java index b67553006..d9d5a0696 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java @@ -6,12 +6,12 @@ import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.util.TestEntityFactory; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.domain.value.Category; import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordPinApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordPinApiTest.java index 647b9c78c..dab31cfc2 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordPinApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordPinApiTest.java @@ -7,7 +7,7 @@ import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordUpdateApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordUpdateApiTest.java index eeea8ad1c..5565fe947 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordUpdateApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RecordUpdateApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java index 2c1ff4f23..7eee4c414 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java @@ -10,11 +10,11 @@ import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.roompost.adapter.out.jpa.VoteItemJpaEntity; import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteApiTest.java index b470527ab..d180f4b30 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteApiTest.java @@ -5,7 +5,7 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java index e16bb8c0b..5cc332584 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java index f57614cea..067a12d60 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteDeleteApiTest.java @@ -6,9 +6,9 @@ import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.util.TestEntityFactory; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteUpdateApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteUpdateApiTest.java index 4c287d1f9..76ee76306 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteUpdateApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/VoteUpdateApiTest.java @@ -6,7 +6,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java b/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java index abcc74362..dbb3603fd 100644 --- a/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java +++ b/src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java @@ -5,7 +5,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java index f3e66fe8c..0a513adcd 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java @@ -14,14 +14,14 @@ import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomStatus; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; +import konkuk.thip.room.domain.value.RoomStatus; import konkuk.thip.roompost.adapter.out.jpa.*; import konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; @@ -49,9 +49,9 @@ import static konkuk.thip.common.entity.StatusType.INACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.USER_CANNOT_DELETE_ROOM_HOST; import static konkuk.thip.post.domain.PostType.*; -import static konkuk.thip.room.adapter.out.jpa.RoomParticipantRole.HOST; -import static konkuk.thip.room.adapter.out.jpa.RoomParticipantRole.MEMBER; -import static konkuk.thip.room.adapter.out.jpa.RoomStatus.*; +import static konkuk.thip.room.domain.value.RoomParticipantRole.HOST; +import static konkuk.thip.room.domain.value.RoomParticipantRole.MEMBER; +import static konkuk.thip.room.domain.value.RoomStatus.*; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java index 57b281484..77814ec56 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java @@ -3,7 +3,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.domain.value.Alias; diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java index 494a22ab7..06be8ae90 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserReactionApiTest.java @@ -9,9 +9,9 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; -import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.post.adapter.out.persistence.repository.PostLikeJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; -import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.domain.value.RoomParticipantRole; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.domain.value.Category; diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java index 92842ba8c..2f4a1946d 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java @@ -2,7 +2,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; -import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; +import konkuk.thip.recentSearch.domain.value.RecentSearchType; import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java index 04225af42..9a929dc52 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java @@ -4,7 +4,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.in.web.request.UserUpdateRequest; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.domain.value.UserRole; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; import org.junit.jupiter.api.AfterEach; diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index fa56158ec..99901b3d6 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -23,7 +23,7 @@ import java.time.LocalDate; import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; -import static konkuk.thip.user.adapter.out.jpa.UserRole.USER; +import static konkuk.thip.user.domain.value.UserRole.USER; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; diff --git a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java index 832e43af2..5f9a23571 100644 --- a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java +++ b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java @@ -5,6 +5,7 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.domain.value.Alias; +import konkuk.thip.user.domain.value.UserRole; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java b/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java index f749f1d5d..84471a5d9 100644 --- a/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java +++ b/src/test/java/konkuk/thip/user/application/service/UserFollowServiceTest.java @@ -1,6 +1,7 @@ package konkuk.thip.user.application.service; import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.message.application.port.out.FeedEventCommandPort; import konkuk.thip.user.application.port.in.dto.UserFollowCommand; import konkuk.thip.user.application.port.out.FollowingCommandPort; import konkuk.thip.user.application.port.out.UserCommandPort; @@ -28,11 +29,14 @@ class UserFollowServiceTest { private UserCommandPort userCommandPort; private UserFollowService userFollowService; + private FeedEventCommandPort feedEventCommandPort; + @BeforeEach void setUp() { followingCommandPort = mock(FollowingCommandPort.class); userCommandPort = mock(UserCommandPort.class); - userFollowService = new UserFollowService(followingCommandPort, userCommandPort); + feedEventCommandPort = mock(FeedEventCommandPort.class); + userFollowService = new UserFollowService(followingCommandPort, userCommandPort, feedEventCommandPort); } @Nested @@ -68,6 +72,7 @@ void follow_newRelation() { User user = createUserWithFollowerCount(0); when(userCommandPort.findById(targetUserId)).thenReturn(user); + when(userCommandPort.findById(userId)).thenReturn(user); // μ•Œλ¦Ό μ „μ†‘μš© UserFollowCommand command = new UserFollowCommand(userId, targetUserId, true);