diff --git a/.github/workflows/admin-ci-cd.yml b/.github/workflows/admin-ci-cd.yml index 4e3daac9..8ef6b7fb 100644 --- a/.github/workflows/admin-ci-cd.yml +++ b/.github/workflows/admin-ci-cd.yml @@ -11,10 +11,6 @@ on: - 'env/**' - '.env' -concurrency: - group: ${{ github.ref_name }} - cancel-in-progress: false - jobs: admin-ci-cd: runs-on: ubuntu-latest @@ -58,19 +54,32 @@ jobs: flags: 'g' - - name: Deploy to Local Server + - name: Wait for deploy lock and deploy admin uses: appleboy/ssh-action@master with: host: ${{ secrets.LOCAL_SERVER_HOST }} username: ${{ secrets.LOCAL_SERVER_USER }} password: ${{ secrets.LOCAL_SERVER_SSH_PASSWORD }} script: | + set -e branchName=${{ github.ref_name }} - echo ${branchName} - echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S chmod +x /home/sluv/${branchName}-server/${branchName}-deploy-script.sh - echo "chmod Clear" - echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S /home/sluv/${branchName}-server/${branchName}-deploy-script.sh - echo "Process Clear" + lockFile="/tmp/sluv-${branchName}-deploy.lock" + + ( + echo "[deploy-lock] admin deploy is waiting for lock: ${lockFile}" + if ! flock -w 1800 200; then + echo "[deploy-lock] admin deploy failed to acquire lock after 1800 seconds" + exit 1 + fi + + echo "[deploy-lock] admin deploy acquired lock" + echo "[deploy] admin deploy started for branch: ${branchName}" + echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S chmod +x /home/sluv/${branchName}-server/${branchName}-deploy-script.sh + echo "[deploy] chmod clear" + echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S /home/sluv/${branchName}-server/${branchName}-deploy-script.sh + echo "[deploy] admin deploy finished" + echo "[deploy-lock] admin deploy releasing lock" + ) 200>${lockFile} current-time: needs: admin-ci-cd @@ -96,4 +105,4 @@ jobs: uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: "Dev ${{ github.ref_name }} Admin-Module Deploy ${{ job.status }}" \ No newline at end of file + content: "Dev ${{ github.ref_name }} Admin-Module Deploy ${{ job.status }}" diff --git a/.github/workflows/api-ci-cd.yml b/.github/workflows/api-ci-cd.yml index 96ed7d56..d9a38094 100644 --- a/.github/workflows/api-ci-cd.yml +++ b/.github/workflows/api-ci-cd.yml @@ -11,10 +11,6 @@ on: - 'env/**' - '.env' -concurrency: - group: ${{ github.ref_name }} - cancel-in-progress: false - jobs: api-ci-cd: runs-on: ubuntu-latest @@ -58,19 +54,32 @@ jobs: flags: 'g' - - name: Deploy to Local Server + - name: Wait for deploy lock and deploy api uses: appleboy/ssh-action@master with: host: ${{ secrets.LOCAL_SERVER_HOST }} username: ${{ secrets.LOCAL_SERVER_USER }} password: ${{ secrets.LOCAL_SERVER_SSH_PASSWORD }} script: | + set -e branchName=${{ github.ref_name }} - echo ${branchName} - echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S chmod +x /home/sluv/${branchName}-server/${branchName}-deploy-script.sh - echo "chmod Clear" - echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S /home/sluv/${branchName}-server/${branchName}-deploy-script.sh - echo "Process Clear" + lockFile="/tmp/sluv-${branchName}-deploy.lock" + + ( + echo "[deploy-lock] api deploy is waiting for lock: ${lockFile}" + if ! flock -w 1800 200; then + echo "[deploy-lock] api deploy failed to acquire lock after 1800 seconds" + exit 1 + fi + + echo "[deploy-lock] api deploy acquired lock" + echo "[deploy] api deploy started for branch: ${branchName}" + echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S chmod +x /home/sluv/${branchName}-server/${branchName}-deploy-script.sh + echo "[deploy] chmod clear" + echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S /home/sluv/${branchName}-server/${branchName}-deploy-script.sh + echo "[deploy] api deploy finished" + echo "[deploy-lock] api deploy releasing lock" + ) 200>${lockFile} current-time: needs: api-ci-cd @@ -96,4 +105,4 @@ jobs: uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: "Dev ${{ github.ref_name }} Api-Module Deploy ${{ job.status }}" \ No newline at end of file + content: "Dev ${{ github.ref_name }} Api-Module Deploy ${{ job.status }}" diff --git a/.github/workflows/batch-ci-cd.yml b/.github/workflows/batch-ci-cd.yml index de837129..3cb4d38c 100644 --- a/.github/workflows/batch-ci-cd.yml +++ b/.github/workflows/batch-ci-cd.yml @@ -11,10 +11,6 @@ on: - 'env/**' - '.env' -concurrency: - group: ${{ github.ref_name }} - cancel-in-progress: false - jobs: batch-ci-cd: runs-on: ubuntu-latest @@ -58,19 +54,32 @@ jobs: flags: 'g' - - name: Deploy to Local Server + - name: Wait for deploy lock and deploy batch uses: appleboy/ssh-action@master with: host: ${{ secrets.LOCAL_SERVER_HOST }} username: ${{ secrets.LOCAL_SERVER_USER }} password: ${{ secrets.LOCAL_SERVER_SSH_PASSWORD }} script: | + set -e branchName=${{ github.ref_name }} - echo ${branchName} - echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S chmod +x /home/sluv/${branchName}-server/${branchName}-deploy-script.sh - echo "chmod Clear" - echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S /home/sluv/${branchName}-server/${branchName}-deploy-script.sh - echo "Process Clear" + lockFile="/tmp/sluv-${branchName}-deploy.lock" + + ( + echo "[deploy-lock] batch deploy is waiting for lock: ${lockFile}" + if ! flock -w 1800 200; then + echo "[deploy-lock] batch deploy failed to acquire lock after 1800 seconds" + exit 1 + fi + + echo "[deploy-lock] batch deploy acquired lock" + echo "[deploy] batch deploy started for branch: ${branchName}" + echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S chmod +x /home/sluv/${branchName}-server/${branchName}-deploy-script.sh + echo "[deploy] chmod clear" + echo ${{ secrets.LOCAL_SERVER_SUDO_PASSWORD }} | sudo -S /home/sluv/${branchName}-server/${branchName}-deploy-script.sh + echo "[deploy] batch deploy finished" + echo "[deploy-lock] batch deploy releasing lock" + ) 200>${lockFile} current-time: needs: batch-ci-cd @@ -96,4 +105,4 @@ jobs: uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: "Dev ${{ github.ref_name }} Batch-Module Deploy ${{ job.status }}" \ No newline at end of file + content: "Dev ${{ github.ref_name }} Batch-Module Deploy ${{ job.status }}" diff --git a/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/CelebSelfPostRequest.java b/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/CelebSelfPostRequest.java index 91253f6b..5fae0084 100644 --- a/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/CelebSelfPostRequest.java +++ b/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/CelebSelfPostRequest.java @@ -3,7 +3,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; @Getter @ToString diff --git a/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/NewCelebSelfPostRequest.java b/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/NewCelebSelfPostRequest.java index 2930f7e7..a9b75f16 100644 --- a/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/NewCelebSelfPostRequest.java +++ b/sluv-admin/src/main/java/com/sluv/admin/celeb/dto/NewCelebSelfPostRequest.java @@ -2,7 +2,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; @Getter @NoArgsConstructor diff --git a/sluv-admin/src/main/java/com/sluv/admin/item/controller/ItemDashBoardController.java b/sluv-admin/src/main/java/com/sluv/admin/item/controller/ItemDashBoardController.java index a5055910..22aeadc2 100644 --- a/sluv-admin/src/main/java/com/sluv/admin/item/controller/ItemDashBoardController.java +++ b/sluv-admin/src/main/java/com/sluv/admin/item/controller/ItemDashBoardController.java @@ -8,7 +8,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; diff --git a/sluv-admin/src/main/java/com/sluv/admin/router/brand/BrandRouter.java b/sluv-admin/src/main/java/com/sluv/admin/router/brand/BrandRouter.java index d25bb6e7..a5aac94f 100644 --- a/sluv-admin/src/main/java/com/sluv/admin/router/brand/BrandRouter.java +++ b/sluv-admin/src/main/java/com/sluv/admin/router/brand/BrandRouter.java @@ -3,7 +3,7 @@ import com.sluv.admin.brand.dto.BrandPageResponse; import com.sluv.admin.brand.service.BrandService; import lombok.RequiredArgsConstructor; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; diff --git a/sluv-admin/src/main/java/com/sluv/admin/router/celeb/CelebRouter.java b/sluv-admin/src/main/java/com/sluv/admin/router/celeb/CelebRouter.java index 340f1f6c..4fe97b08 100644 --- a/sluv-admin/src/main/java/com/sluv/admin/router/celeb/CelebRouter.java +++ b/sluv-admin/src/main/java/com/sluv/admin/router/celeb/CelebRouter.java @@ -5,7 +5,7 @@ import com.sluv.admin.celeb.service.CelebCategoryService; import com.sluv.admin.celeb.service.CelebService; import lombok.RequiredArgsConstructor; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; diff --git a/sluv-api/src/main/java/com/sluv/api/auth/controller/AuthController.java b/sluv-api/src/main/java/com/sluv/api/auth/controller/AuthController.java index 6515efae..d5077228 100644 --- a/sluv-api/src/main/java/com/sluv/api/auth/controller/AuthController.java +++ b/sluv-api/src/main/java/com/sluv/api/auth/controller/AuthController.java @@ -17,7 +17,7 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.web.bind.annotation.*; @RestController diff --git a/sluv-api/src/main/java/com/sluv/api/auth/dto/request/AuthRequest.java b/sluv-api/src/main/java/com/sluv/api/auth/dto/request/AuthRequest.java index 3c47b993..88ccaec9 100644 --- a/sluv-api/src/main/java/com/sluv/api/auth/dto/request/AuthRequest.java +++ b/sluv-api/src/main/java/com/sluv/api/auth/dto/request/AuthRequest.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; @Getter @NoArgsConstructor diff --git a/sluv-api/src/main/java/com/sluv/api/closet/controller/ClosetController.java b/sluv-api/src/main/java/com/sluv/api/closet/controller/ClosetController.java index f4941dfd..a84aa70f 100644 --- a/sluv-api/src/main/java/com/sluv/api/closet/controller/ClosetController.java +++ b/sluv-api/src/main/java/com/sluv/api/closet/controller/ClosetController.java @@ -10,7 +10,7 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.web.bind.annotation.*; @RestController diff --git a/sluv-api/src/main/java/com/sluv/api/moderation/service/QuestionModerationService.java b/sluv-api/src/main/java/com/sluv/api/moderation/service/QuestionModerationService.java new file mode 100644 index 00000000..de7eec92 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/moderation/service/QuestionModerationService.java @@ -0,0 +1,24 @@ +package com.sluv.api.moderation.service; + +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import com.sluv.domain.featureflag.service.FeatureFlagDomainService; +import com.sluv.domain.moderation.service.ModerationJobDomainService; +import com.sluv.domain.question.entity.Question; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class QuestionModerationService { + + private final FeatureFlagDomainService featureFlagDomainService; + private final ModerationJobDomainService moderationJobDomainService; + + public void createQuestionJobIfEnabled(Question question) { + if (!featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION)) { + return; + } + + moderationJobDomainService.createQuestionJobIfAbsent(question.getId(), question.getUser().getId()); + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/moderation/service/QuestionModerationStatusService.java b/sluv-api/src/main/java/com/sluv/api/moderation/service/QuestionModerationStatusService.java new file mode 100644 index 00000000..aa9b1290 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/moderation/service/QuestionModerationStatusService.java @@ -0,0 +1,30 @@ +package com.sluv.api.moderation.service; + +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import com.sluv.domain.featureflag.service.FeatureFlagDomainService; +import com.sluv.domain.question.enums.QuestionStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class QuestionModerationStatusService { + + private final FeatureFlagDomainService featureFlagDomainService; + + public QuestionStatus getInitialQuestionStatus() { + if (featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_CREATE_PENDING)) { + return QuestionStatus.PENDING; + } + + return QuestionStatus.ACTIVE; + } + + public QuestionStatus getUpdateQuestionStatus() { + if (featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_UPDATE_PENDING)) { + return QuestionStatus.PENDING; + } + + return QuestionStatus.ACTIVE; + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/controller/QuestionController.java b/sluv-api/src/main/java/com/sluv/api/question/controller/QuestionController.java index 651a4da1..ddffab24 100644 --- a/sluv-api/src/main/java/com/sluv/api/question/controller/QuestionController.java +++ b/sluv-api/src/main/java/com/sluv/api/question/controller/QuestionController.java @@ -4,7 +4,13 @@ import com.sluv.api.common.response.SuccessDataResponse; import com.sluv.api.common.response.SuccessResponse; import com.sluv.api.question.dto.*; +import com.sluv.api.question.service.QuestionFeedService; +import com.sluv.api.question.service.QuestionLikeService; +import com.sluv.api.question.service.QuestionRankService; +import com.sluv.api.question.service.QuestionReportService; import com.sluv.api.question.service.QuestionService; +import com.sluv.api.question.service.QuestionVoteService; +import com.sluv.api.question.service.QuestionWaitService; import com.sluv.common.annotation.CurrentUserId; import com.sluv.domain.question.dto.QuestionSimpleResDto; import com.sluv.domain.question.exception.QuestionTypeNotFoundException; @@ -12,7 +18,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,6 +28,12 @@ @RequestMapping("/app/question") public class QuestionController { private final QuestionService questionService; + private final QuestionFeedService questionFeedService; + private final QuestionLikeService questionLikeService; + private final QuestionRankService questionRankService; + private final QuestionReportService questionReportService; + private final QuestionVoteService questionVoteService; + private final QuestionWaitService questionWaitService; @Operation(summary = "*찾아주세요 게시글 등록", description = "User 토큰 필요. 생성: id -> null. 수정: id -> 해당 Question Id") @@ -74,7 +86,7 @@ public ResponseEntity deleteQuestion(@PathVariable("questionId" @PostMapping("/{questionId}/like") public ResponseEntity postQuestionLike(@CurrentUserId Long userId, @PathVariable("questionId") Long questionId) { - questionService.postQuestionLike(userId, questionId); + questionLikeService.postQuestionLike(userId, questionId); return ResponseEntity.ok().body(SuccessResponse.create()); } @@ -84,7 +96,7 @@ public ResponseEntity postQuestionLike(@CurrentUserId Long user public ResponseEntity postQuestionReport(@CurrentUserId Long userId, @PathVariable("questionId") Long questionId, @RequestBody QuestionReportReqDto dto) { - questionService.postQuestionReport(userId, questionId, dto); + questionReportService.postQuestionReport(userId, questionId, dto); return ResponseEntity.ok().body(SuccessResponse.create()); } @@ -102,7 +114,7 @@ public ResponseEntity> postQuestion public ResponseEntity postQuestionVote(@CurrentUserId Long userId, @PathVariable("questionId") Long questionId, @RequestBody QuestionVoteReqDto dto) { - questionService.postQuestionVote(userId, questionId, dto); + questionVoteService.postQuestionVote(userId, questionId, dto); return ResponseEntity.ok().body(SuccessResponse.create()); } @@ -114,10 +126,10 @@ public ResponseEntity>> getWaitQu @RequestParam("questionId") Long questionId, @RequestParam("qType") String qType) { List result = switch (qType) { - case "Buy" -> questionService.getWaitQuestionBuy(userId, questionId); - case "Find" -> questionService.getWaitQuestionFind(userId, questionId); - case "How" -> questionService.getWaitQuestionHowabout(userId, questionId); - case "Recommend" -> questionService.getWaitQuestionRecommend(userId, questionId); + case "Buy" -> questionWaitService.getWaitQuestionBuy(userId, questionId); + case "Find" -> questionWaitService.getWaitQuestionFind(userId, questionId); + case "How" -> questionWaitService.getWaitQuestionHowabout(userId, questionId); + case "Recommend" -> questionWaitService.getWaitQuestionRecommend(userId, questionId); default -> throw new QuestionTypeNotFoundException(); }; @@ -126,20 +138,20 @@ public ResponseEntity>> getWaitQu @Operation(summary = "Question 커뮤니티 게시글 종합 검색", description = "Pagination 적용. 최신순으로 조회") @GetMapping("/total") - public ResponseEntity>> getQuestionTotalList( + public ResponseEntity>> getTotalQuestions( @CurrentUserId Long userId, Pageable pageable) { - PaginationResponse response = questionService.getTotalQuestionList(userId, pageable); + PaginationResponse response = questionFeedService.getTotalQuestions(userId, pageable); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } @Operation(summary = "QuestionFind 커뮤니티 게시글 검색", description = "Pagination 적용. Ordering: 최신순으로 조회. Filtering: celebId.\n isNewCeleb이 null일 경우 정식 셀럽으로 간주") @GetMapping("/find") - public ResponseEntity>> getQuestionFindList( + public ResponseEntity>> getFindQuestions( @CurrentUserId Long userId, @Nullable @RequestParam("celebId") Long celebId, @Nullable @RequestParam("isNewCeleb") Boolean isNewCeleb, Pageable pageable) { - PaginationResponse response = questionService.getQuestionFindList(userId, celebId, + PaginationResponse response = questionFeedService.getFindQuestions(userId, celebId, isNewCeleb, pageable); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } @@ -157,19 +169,19 @@ public ResponseEntity>> getQuestionBuyList( + public ResponseEntity>> getBuyQuestions( @CurrentUserId Long userId, @Nullable @RequestParam("voteStatus") String voteStatus, Pageable pageable) { - PaginationResponse response = questionService.getQuestionBuyList(userId, + PaginationResponse response = questionFeedService.getBuyQuestions(userId, voteStatus, pageable); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } @Operation(summary = "QuestionHowabout 커뮤니티 게시글 검색", description = "Pagination 적용. Ordering 최신순") @GetMapping("/howabout") - public ResponseEntity>> getQuestionHowaboutList( + public ResponseEntity>> getHowaboutQuestions( @CurrentUserId Long userId, Pageable pageable) { - PaginationResponse response = questionService.getQuestionHowaboutList( + PaginationResponse response = questionFeedService.getHowaboutQuestions( userId, pageable); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } @@ -177,9 +189,9 @@ public ResponseEntity>> getQuestionRecommendList( + public ResponseEntity>> getRecommendQuestions( @CurrentUserId Long userId, @Nullable @RequestParam String hashtag, Pageable pageable) { - PaginationResponse response = questionService.getQuestionRecommendList( + PaginationResponse response = questionFeedService.getRecommendQuestions( userId, hashtag, pageable); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } @@ -187,17 +199,17 @@ public ResponseEntity>> getDailyHotQuestionList(@CurrentUserId Long userId) { - List response = questionService.getDailyHotQuestionList(userId); + public ResponseEntity>> getDailyHotQuestions(@CurrentUserId Long userId) { + List response = questionRankService.getDailyHotQuestions(userId); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } @Operation(summary = "주간 핫 커뮤니티 게시글 검색", description = "Pagination 적용. Ordering 조회수 + 좋아요 수 + 댓글 수. Filtering 현재를 기점으로 일주일간 작성된 글") @GetMapping("/weeklyhot") - public ResponseEntity>> getWeeklyHotQuestionList( + public ResponseEntity>> getWeeklyHotQuestions( @CurrentUserId Long userId, Pageable pageable) { - PaginationResponse response = questionService.getWeeklyHotQuestionList( + PaginationResponse response = questionRankService.getWeeklyHotQuestions( userId, pageable); return ResponseEntity.ok().body(SuccessDataResponse.from(response)); } diff --git a/sluv-api/src/main/java/com/sluv/api/question/dto/QuestionDetailTypeData.java b/sluv-api/src/main/java/com/sluv/api/question/dto/QuestionDetailTypeData.java new file mode 100644 index 00000000..60185076 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/dto/QuestionDetailTypeData.java @@ -0,0 +1,31 @@ +package com.sluv.api.question.dto; + +import com.sluv.api.celeb.dto.response.CelebChipResponse; +import java.time.LocalDateTime; +import java.util.List; + +public record QuestionDetailTypeData( + CelebChipResponse celeb, + CelebChipResponse newCeleb, + LocalDateTime voteEndTime, + Long totalVoteNum, + Long voteStatus, + List recommendCategories +) { + + public static QuestionDetailTypeData empty() { + return new QuestionDetailTypeData(null, null, null, null, null, null); + } + + public static QuestionDetailTypeData ofCeleb(CelebChipResponse celeb, CelebChipResponse newCeleb) { + return new QuestionDetailTypeData(celeb, newCeleb, null, null, null, null); + } + + public static QuestionDetailTypeData ofVote(LocalDateTime voteEndTime, Long totalVoteNum, Long voteStatus) { + return new QuestionDetailTypeData(null, null, voteEndTime, totalVoteNum, voteStatus, null); + } + + public static QuestionDetailTypeData ofRecommendCategories(List recommendCategories) { + return new QuestionDetailTypeData(null, null, null, null, null, recommendCategories); + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionImageManager.java b/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionImageManager.java new file mode 100644 index 00000000..1511a549 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionImageManager.java @@ -0,0 +1,152 @@ +package com.sluv.api.question.helper; + +import com.sluv.api.question.dto.QuestionImgReqDto; +import com.sluv.api.question.dto.QuestionImgResDto; +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.domain.item.entity.ItemImg; +import com.sluv.domain.item.repository.ItemImgRepository; +import com.sluv.domain.question.dto.QuestionImgSimpleDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionImg; +import com.sluv.domain.question.entity.QuestionItem; +import com.sluv.domain.question.repository.QuestionImgRepository; +import com.sluv.domain.question.repository.QuestionItemRepository; +import com.sluv.domain.question.service.QuestionImgDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +@Component +@RequiredArgsConstructor +public class QuestionImageManager { + + private final QuestionImgDomainService questionImgDomainService; + private final QuestionImgRepository questionImgRepository; + private final QuestionItemRepository questionItemRepository; + private final ItemImgRepository itemImgRepository; + + public void saveImages(List imageRequests, Question question) { + questionImgDomainService.deleteAllByQuestionId(question.getId()); + + if (imageRequests != null) { + List questionImages = imageRequests.stream() + .map(imageRequest -> QuestionImg.toEntity( + question, + imageRequest.getImgUrl(), + imageRequest.getDescription(), + imageRequest.getRepresentFlag(), + imageRequest.getSortOrder() + )) + .toList(); + + questionImgDomainService.saveAll(questionImages); + } + } + + public List getQuestionImageResponses(Long questionId, + Function voteDataResolver) { + return questionImgRepository.findAllByQuestionId(questionId) + .stream() + .map(questionImg -> QuestionImgResDto.of(questionImg, voteDataResolver.apply(questionImg.getSortOrder()))) + .toList(); + } + + public List getQuestionImages(Question question) { + if (!(question instanceof QuestionBuy)) { + return null; + } + + return getAllQuestionImages(question); + } + + public List getAllQuestionImages(Question question) { + return questionImgRepository.findAllByQuestionId(question.getId()).stream() + .map(QuestionImgSimpleDto::of) + .toList(); + } + + public List getQuestionImagesWithMainImage(Question question) { + if (question instanceof QuestionBuy) { + return getQuestionImages(question); + } + + List questionImages = new ArrayList<>(); + QuestionImg mainQuestionImage = questionImgRepository.findByQuestionIdAndRepresentFlag(question.getId(), true); + if (mainQuestionImage != null) { + questionImages.add(QuestionImgSimpleDto.of(mainQuestionImage)); + } + + QuestionImgSimpleDto itemMainImage = getItemMainImage(question); + if (itemMainImage != null) { + questionImages.add(itemMainImage); + } + + return questionImages; + } + + public List getBuyItemMainImages(Question question) { + if (!(question instanceof QuestionBuy)) { + return null; + } + + return getAllItemMainImages(question); + } + + public List getAllItemMainImages(Question question) { + return questionItemRepository.findAllByQuestionId(question.getId()).stream() + .map(questionItem -> { + ItemImg mainImg = itemImgRepository.findMainImg(questionItem.getItem().getId()); + return QuestionImgSimpleDto.of(mainImg); + }) + .toList(); + } + + public List getItemMainImagesForCard(Question question) { + if (!(question instanceof QuestionBuy)) { + return new ArrayList<>(); + } + + return getBuyItemMainImages(question); + } + + public QuestionImgSimpleDto getItemMainImage(Question question) { + QuestionItem mainQuestionItem = questionItemRepository.findByQuestionIdAndRepresentFlag(question.getId(), true); + if (mainQuestionItem == null) { + return null; + } + + ItemImg mainItemImage = itemImgRepository.findMainImg(mainQuestionItem.getItem().getId()); + return QuestionImgSimpleDto.of(mainItemImage); + } + + public List getQuestionImagesForHome(Question question) { + if (question instanceof QuestionBuy) { + List questionImages = new ArrayList<>(); + questionImages.addAll(getQuestionImages(question)); + questionImages.addAll(getBuyItemMainImages(question)); + questionImages.sort(Comparator.comparing(QuestionImgSimpleDto::getSortOrder)); + + return questionImages; + } + + QuestionImg mainQuestionImage = questionImgRepository.findByQuestionIdAndRepresentFlag(question.getId(), true); + QuestionItem mainQuestionItem = questionItemRepository.findByQuestionIdAndRepresentFlag(question.getId(), true); + String imageUrl = null; + + if (mainQuestionImage != null) { + imageUrl = mainQuestionImage.getImgUrl(); + } else if (mainQuestionItem != null) { + imageUrl = itemImgRepository.findMainImg(mainQuestionItem.getItem().getId()).getItemImgUrl(); + } + + return imageUrl != null + ? Arrays.asList(new QuestionImgSimpleDto(imageUrl, 0L)) + : null; + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionItemManager.java b/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionItemManager.java new file mode 100644 index 00000000..b6f6ddd0 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionItemManager.java @@ -0,0 +1,70 @@ +package com.sluv.api.question.helper; + +import com.sluv.api.question.dto.QuestionItemResDto; +import com.sluv.api.question.dto.QuestionItemReqDto; +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.domain.closet.entity.Closet; +import com.sluv.domain.item.dto.ItemSimpleDto; +import com.sluv.domain.item.entity.Item; +import com.sluv.domain.item.service.ItemDomainService; +import com.sluv.domain.item.service.ItemImgDomainService; +import com.sluv.domain.item.service.ItemScrapDomainService; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.entity.QuestionItem; +import com.sluv.domain.question.service.QuestionItemDomainService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.function.Function; + +@Component +@RequiredArgsConstructor +public class QuestionItemManager { + + private final QuestionItemDomainService questionItemDomainService; + private final ItemDomainService itemDomainService; + private final ItemImgDomainService itemImgDomainService; + private final ItemScrapDomainService itemScrapDomainService; + + public void saveItems(List itemRequests, Question question) { + questionItemDomainService.deleteAllByQuestionId(question.getId()); + + if (itemRequests != null) { + List questionItems = itemRequests.stream() + .map(itemRequest -> { + Item item = itemDomainService.findById(itemRequest.getItemId()); + + return QuestionItem.toEntity( + question, + item, + itemRequest.getDescription(), + itemRequest.getRepresentFlag(), + itemRequest.getSortOrder() + ); + }) + .toList(); + + questionItemDomainService.saveAll(questionItems); + } + } + + public List getQuestionItemResponses(Long questionId, List closets, + Function voteDataResolver) { + return questionItemDomainService.findAllByQuestionId(questionId) + .stream() + .map(questionItem -> getQuestionItemResponse(questionItem, closets, voteDataResolver)) + .toList(); + } + + private QuestionItemResDto getQuestionItemResponse(QuestionItem questionItem, List closets, + Function voteDataResolver) { + ItemSimpleDto item = ItemSimpleDto.of( + questionItem.getItem(), + itemImgDomainService.findMainImg(questionItem.getItem().getId()), + itemScrapDomainService.getItemScrapStatus(questionItem.getItem(), closets) + ); + + return QuestionItemResDto.of(questionItem, item, voteDataResolver.apply(questionItem.getSortOrder())); + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionResponseAssembler.java b/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionResponseAssembler.java new file mode 100644 index 00000000..4438f271 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/helper/QuestionResponseAssembler.java @@ -0,0 +1,125 @@ +package com.sluv.api.question.helper; + +import com.sluv.api.question.dto.QuestionHomeResDto; +import com.sluv.domain.comment.repository.CommentRepository; +import com.sluv.domain.question.dto.QuestionImgSimpleDto; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionFind; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.entity.QuestionRecommendCategory; +import com.sluv.domain.question.exception.QuestionTypeNotFoundException; +import com.sluv.domain.question.repository.QuestionLikeRepository; +import com.sluv.domain.question.repository.QuestionRecommendCategoryRepository; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.repository.UserRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class QuestionResponseAssembler { + + private final QuestionImageManager questionImageManager; + private final QuestionLikeRepository questionLikeRepository; + private final QuestionRecommendCategoryRepository questionRecommendCategoryRepository; + private final CommentRepository commentRepository; + private final UserRepository userRepository; + + public QuestionSimpleResDto getQuestionSimpleResponse(Question question) { + validateQuestionType(question); + + List questionImages = questionImageManager.getQuestionImages(question); + List itemMainImages = questionImageManager.getBuyItemMainImages(question); + List recommendCategoryNames = getRecommendCategoryNames(question); + Long likeNum = questionLikeRepository.countByQuestionId(question.getId()); + Long commentNum = commentRepository.countByQuestionId(question.getId()); + User writer = userRepository.findById(question.getUser().getId()).orElse(null); + + return QuestionSimpleResDto.of( + question, + writer, + likeNum, + commentNum, + questionImages, + itemMainImages, + recommendCategoryNames + ); + } + + public QuestionSimpleResDto getQuestionSimpleResponseWithMainImage(Question question) { + validateQuestionType(question); + + List questionImages = questionImageManager.getQuestionImagesWithMainImage(question); + List itemMainImages = questionImageManager.getItemMainImagesForCard(question); + List recommendCategoryNames = getRecommendCategoryNames(question); + Long likeNum = questionLikeRepository.countByQuestionId(question.getId()); + Long commentNum = commentRepository.countByQuestionId(question.getId()); + User writer = userRepository.findById(question.getUser().getId()).orElse(null); + + return QuestionSimpleResDto.of( + question, + writer, + likeNum, + commentNum, + questionImages, + itemMainImages, + recommendCategoryNames + ); + } + + public QuestionHomeResDto getQuestionHomeResponse(Question question) { + validateQuestionType(question); + + List questionImages = questionImageManager.getQuestionImagesForHome(question); + User writer = userRepository.findById(question.getUser().getId()).orElse(null); + + return QuestionHomeResDto.of(question, writer, questionImages); + } + + public QuestionSimpleResDto getQuestionSimpleResponseWithImages(Question question) { + validateQuestionType(question); + + List questionImages = questionImageManager.getAllQuestionImages(question); + List itemMainImages = questionImageManager.getAllItemMainImages(question); + List recommendCategoryNames = getRecommendCategoryNames(question); + Long likeNum = questionLikeRepository.countByQuestionId(question.getId()); + Long commentNum = commentRepository.countByQuestionId(question.getId()); + User writer = userRepository.findById(question.getUser().getId()).orElse(null); + + return QuestionSimpleResDto.of( + question, + writer, + likeNum, + commentNum, + questionImages, + itemMainImages, + recommendCategoryNames + ); + } + + private void validateQuestionType(Question question) { + if (question instanceof QuestionBuy + || question instanceof QuestionFind + || question instanceof QuestionHowabout + || question instanceof QuestionRecommend) { + return; + } + + throw new QuestionTypeNotFoundException(); + } + + + private List getRecommendCategoryNames(Question question) { + if (!(question instanceof QuestionRecommend)) { + return null; + } + + return questionRecommendCategoryRepository.findAllByQuestionId(question.getId()).stream() + .map(QuestionRecommendCategory::getName) + .toList(); + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/mapper/QuestionDtoMapper.java b/sluv-api/src/main/java/com/sluv/api/question/mapper/QuestionDtoMapper.java deleted file mode 100644 index 1dfa2345..00000000 --- a/sluv-api/src/main/java/com/sluv/api/question/mapper/QuestionDtoMapper.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.sluv.api.question.mapper; - -import com.sluv.domain.comment.repository.CommentRepository; -import com.sluv.domain.item.entity.ItemImg; -import com.sluv.domain.item.repository.ItemImgRepository; -import com.sluv.domain.question.dto.QuestionImgSimpleDto; -import com.sluv.domain.question.dto.QuestionSimpleResDto; -import com.sluv.domain.question.entity.Question; -import com.sluv.domain.question.entity.QuestionBuy; -import com.sluv.domain.question.entity.QuestionFind; -import com.sluv.domain.question.entity.QuestionHowabout; -import com.sluv.domain.question.entity.QuestionRecommend; -import com.sluv.domain.question.entity.QuestionRecommendCategory; -import com.sluv.domain.question.exception.QuestionTypeNotFoundException; -import com.sluv.domain.question.repository.QuestionImgRepository; -import com.sluv.domain.question.repository.QuestionItemRepository; -import com.sluv.domain.question.repository.QuestionLikeRepository; -import com.sluv.domain.question.repository.QuestionRecommendCategoryRepository; -import com.sluv.domain.user.entity.User; -import com.sluv.domain.user.repository.UserRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class QuestionDtoMapper { - - private final ItemImgRepository itemImgRepository; - private final QuestionImgRepository questionImgRepository; - private final QuestionItemRepository questionItemRepository; - private final QuestionLikeRepository questionLikeRepository; - private final QuestionRecommendCategoryRepository questionRecommendCategoryRepository; - private final CommentRepository commentRepository; - private final UserRepository userRepository; - - public QuestionSimpleResDto dtoBuildByQuestionType(Question question) { - String qType = null; - List imgList = null; - List itemImgList = null; - String celebName = null; - List categoryList = null; - - if (question instanceof QuestionBuy) { // 1. question이 QuestionBuy 일 경우 - // 이미지 DTO 생성 - List imgSimpleList = questionImgRepository.findAllByQuestionId(question.getId()) - .stream().map(QuestionImgSimpleDto::of).toList(); - // 아이템 이미지 DTO 생성 - List itemImgSimpleList = questionItemRepository.findAllByQuestionId( - question.getId()).stream().map(questionItem -> { - ItemImg mainImg = itemImgRepository.findMainImg(questionItem.getItem().getId()); - return QuestionImgSimpleDto.of(mainImg); - }).toList(); - - qType = "Buy"; - imgList = imgSimpleList; - itemImgList = itemImgSimpleList; - - } else if (question instanceof QuestionFind questionFind) { // 2. question이 QuestionFind 일 경우 - qType = "Find"; - celebName = questionFind.getCeleb() != null ? questionFind.getCeleb().getParent() != null ? - questionFind.getCeleb().getParent().getCelebNameKr() + " " + questionFind.getCeleb() - .getCelebNameKr() : questionFind.getCeleb().getCelebNameKr() - : questionFind.getNewCeleb().getCelebName(); - - } else if (question instanceof QuestionHowabout) { // 3. question이 QuestionHowabout 일 경우 - qType = "How"; - - } else if (question instanceof QuestionRecommend) { // 4. question이 QuestionRecommend 일 경우 - List categoryNameList = questionRecommendCategoryRepository.findAllByQuestionId(question.getId()) - .stream().map(QuestionRecommendCategory::getName).toList(); - qType = "Recommend"; - categoryList = categoryNameList; - - } else { - throw new QuestionTypeNotFoundException(); - } - - // Question 좋아요 수 - Long likeNum = questionLikeRepository.countByQuestionId(question.getId()); - - // Question 댓글 수 - Long commentNum = commentRepository.countByQuestionId(question.getId()); - - User writer = userRepository.findById(question.getUser().getId()).orElse(null); - - return QuestionSimpleResDto.of(question, writer, likeNum, commentNum, imgList, itemImgList, categoryList); - } -} diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionFeedService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionFeedService.java new file mode 100644 index 00000000..b15a14f5 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionFeedService.java @@ -0,0 +1,184 @@ +package com.sluv.api.question.service; + +import com.sluv.api.common.response.PaginationResponse; +import com.sluv.api.question.dto.QuestionBuySimpleResDto; +import com.sluv.api.question.dto.QuestionImgResDto; +import com.sluv.api.question.dto.QuestionItemResDto; +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.domain.item.dto.ItemSimpleDto; +import com.sluv.domain.item.service.ItemImgDomainService; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionFind; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionImg; +import com.sluv.domain.question.entity.QuestionItem; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.entity.QuestionVote; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionImgDomainService; +import com.sluv.domain.question.service.QuestionItemDomainService; +import com.sluv.domain.question.service.QuestionVoteDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserBlockDomainService; +import com.sluv.domain.user.service.UserDomainService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class QuestionFeedService { + + private final QuestionDomainService questionDomainService; + private final UserBlockDomainService userBlockDomainService; + private final QuestionResponseAssembler questionResponseAssembler; + private final QuestionImgDomainService questionImgDomainService; + private final QuestionItemDomainService questionItemDomainService; + private final ItemImgDomainService itemImgDomainService; + private final QuestionVoteDomainService questionVoteDomainService; + private final UserDomainService userDomainService; + private final QuestionVoteService questionVoteService; + + @Transactional(readOnly = true) + public PaginationResponse getTotalQuestions(Long userId, Pageable pageable) { + List blockedUserIds = getBlockedUserIds(userId); + Page questions = questionDomainService.getTotalQuestionList(blockedUserIds, pageable); + + return toQuestionSimpleResponse(questions); + } + + @Transactional(readOnly = true) + public PaginationResponse getFindQuestions(Long userId, Long celebId, Boolean isNewCeleb, + Pageable pageable) { + List blockedUserIds = getBlockedUserIds(userId); + Page questions = questionDomainService.getQuestionFindList( + celebId, + isNewCeleb, + blockedUserIds, + pageable + ); + + return toQuestionSimpleResponse(questions); + } + + @Transactional(readOnly = true) + public PaginationResponse getHowaboutQuestions(Long userId, Pageable pageable) { + List blockedUserIds = getBlockedUserIds(userId); + Page questions = questionDomainService.getQuestionHowaboutList(blockedUserIds, pageable); + + return toQuestionSimpleResponse(questions); + } + + @Transactional(readOnly = true) + public PaginationResponse getRecommendQuestions(Long userId, String hashtag, + Pageable pageable) { + List blockedUserIds = getBlockedUserIds(userId); + Page questions = questionDomainService.getQuestionRecommendList( + hashtag, + blockedUserIds, + pageable + ); + + return toQuestionSimpleResponse(questions); + } + + @Transactional(readOnly = true) + public PaginationResponse getBuyQuestions(Long userId, String voteStatus, + Pageable pageable) { + User user = userDomainService.findById(userId); + List blockedUserIds = getBlockedUserIds(userId); + Page questions = questionDomainService.getQuestionBuyList(voteStatus, blockedUserIds, pageable); + List questionResponses = questions.stream() + .map(question -> getBuyQuestionResponse(user, question)) + .toList(); + + return PaginationResponse.of(questions, questionResponses); + } + + private List getBlockedUserIds(Long userId) { + if (userId == null) { + return List.of(); + } + + return userBlockDomainService.getAllBlockedUser(userId).stream() + .map(userBlock -> userBlock.getBlockedUser().getId()) + .toList(); + } + + private PaginationResponse toQuestionSimpleResponse(Page questions) { + List questionResponses = questions.stream() + .map(questionResponseAssembler::getQuestionSimpleResponseWithMainImage) + .toList(); + + return PaginationResponse.of(questions, questionResponses); + } + + private QuestionBuySimpleResDto getBuyQuestionResponse(User user, QuestionBuy question) { + List questionImages = getBuyQuestionImages(question); + List questionItems = getBuyQuestionItems(question); + Long voteCount = questionVoteService.getTotalVoteCount(questionImages, questionItems); + QuestionVote questionVote = getQuestionVote(user, question); + User writer = userDomainService.findByIdOrNull(question.getUser().getId()); + + return QuestionBuySimpleResDto.of( + user, + question, + writer, + voteCount, + questionImages, + questionItems, + question.getVoteEndTime(), + questionVote + ); + } + + private List getBuyQuestionImages(QuestionBuy question) { + return questionImgDomainService.findAllByQuestionId(question.getId()).stream() + .map(this::getBuyQuestionImage) + .toList(); + } + + private QuestionImgResDto getBuyQuestionImage(QuestionImg questionImg) { + QuestionVoteDataDto voteData = questionVoteService.getVoteData( + questionImg.getQuestion().getId(), + (long) questionImg.getSortOrder() + ); + + return QuestionImgResDto.of(questionImg, voteData); + } + + private List getBuyQuestionItems(QuestionBuy question) { + return questionItemDomainService.findAllByQuestionId(question.getId()).stream() + .map(this::getBuyQuestionItem) + .toList(); + } + + private QuestionItemResDto getBuyQuestionItem(QuestionItem questionItem) { + ItemSimpleDto item = ItemSimpleDto.of( + questionItem.getItem(), + itemImgDomainService.findMainImg(questionItem.getItem().getId()), + null + ); + QuestionVoteDataDto voteData = questionVoteService.getVoteData( + questionItem.getQuestion().getId(), + (long) questionItem.getSortOrder() + ); + + return QuestionItemResDto.of(questionItem, item, voteData); + } + + private QuestionVote getQuestionVote(User user, QuestionBuy question) { + if (user == null) { + return null; + } + + return questionVoteDomainService.findByQuestionIdAndUserIdOrNull(question.getId(), user.getId()); + } + +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionLikeService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionLikeService.java new file mode 100644 index 00000000..5aef08f8 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionLikeService.java @@ -0,0 +1,39 @@ +package com.sluv.api.question.service; + +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionLikeDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import com.sluv.infra.alarm.service.QuestionAlarmService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class QuestionLikeService { + + private final UserDomainService userDomainService; + private final QuestionDomainService questionDomainService; + private final QuestionLikeDomainService questionLikeDomainService; + private final QuestionAlarmService questionAlarmService; + + @Transactional + public void postQuestionLike(Long userId, Long questionId) { + User user = userDomainService.findById(userId); + log.info("질문 게시글 좋아요 - 사용자 : {}, 질문 게시글 : {}", user.getId(), questionId); + + Boolean likeStatus = questionLikeDomainService.existsByQuestionIdAndUserId(questionId, user.getId()); + Question question = questionDomainService.findById(questionId); + + if (likeStatus) { + questionLikeDomainService.deleteByQuestionIdAndUserId(questionId, user.getId()); + } else { + questionLikeDomainService.saveQuestionLike(user, question); + questionAlarmService.sendAlarmAboutQuestionLike(user.getId(), question.getId()); + } + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionRankService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionRankService.java new file mode 100644 index 00000000..a42ae532 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionRankService.java @@ -0,0 +1,57 @@ +package com.sluv.api.question.service; + +import com.sluv.api.common.response.PaginationResponse; +import com.sluv.api.question.dto.QuestionHomeResDto; +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.user.service.UserBlockDomainService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class QuestionRankService { + + private final QuestionDomainService questionDomainService; + private final UserBlockDomainService userBlockDomainService; + private final QuestionResponseAssembler questionResponseAssembler; + + @Transactional(readOnly = true) + public List getDailyHotQuestions(Long userId) { + List blockedUserIds = getBlockedUserIds(userId); + List questions = questionDomainService.getDailyHotQuestion(blockedUserIds); + + return questions.stream() + .map(questionResponseAssembler::getQuestionHomeResponse) + .toList(); + } + + @Transactional(readOnly = true) + public PaginationResponse getWeeklyHotQuestions(Long userId, Pageable pageable) { + List blockedUserIds = getBlockedUserIds(userId); + Page questions = questionDomainService.getWeeklyHotQuestion(blockedUserIds, pageable); + + List questionResponses = questions.stream() + .map(questionResponseAssembler::getQuestionSimpleResponseWithImages) + .toList(); + + return PaginationResponse.of(questions, questionResponses); + } + + private List getBlockedUserIds(Long userId) { + if (userId == null) { + return List.of(); + } + + return userBlockDomainService.getAllBlockedUser(userId).stream() + .map(userBlock -> userBlock.getBlockedUser().getId()) + .toList(); + } + +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionReportService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionReportService.java new file mode 100644 index 00000000..c9405abb --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionReportService.java @@ -0,0 +1,37 @@ +package com.sluv.api.question.service; + +import com.sluv.api.question.dto.QuestionReportReqDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.exception.QuestionReportDuplicateException; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionReportDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class QuestionReportService { + + private final UserDomainService userDomainService; + private final QuestionDomainService questionDomainService; + private final QuestionReportDomainService questionReportDomainService; + + @Transactional + public void postQuestionReport(Long userId, Long questionId, QuestionReportReqDto dto) { + User user = userDomainService.findById(userId); + log.info("질문 게시글 신고 - 사용자 : {}, 질문 게시글 : {}, 사유 : {}", user.getId(), questionId, dto.getReason()); + + Boolean reportExists = questionReportDomainService.existsByQuestionIdAndReporterId(questionId, user.getId()); + if (reportExists) { + throw new QuestionReportDuplicateException(); + } + + Question question = questionDomainService.findById(questionId); + questionReportDomainService.saveQuestionReport(user, question, dto.getReason(), dto.getContent()); + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionService.java index 5e848630..567b2e67 100644 --- a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionService.java +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionService.java @@ -1,8 +1,11 @@ package com.sluv.api.question.service; import com.sluv.api.celeb.dto.response.CelebChipResponse; -import com.sluv.api.common.response.PaginationResponse; +import com.sluv.api.moderation.service.QuestionModerationService; +import com.sluv.api.moderation.service.QuestionModerationStatusService; import com.sluv.api.question.dto.*; +import com.sluv.api.question.helper.QuestionImageManager; +import com.sluv.api.question.helper.QuestionItemManager; import com.sluv.domain.celeb.entity.Celeb; import com.sluv.domain.celeb.entity.NewCeleb; import com.sluv.domain.celeb.service.CelebDomainService; @@ -10,58 +13,40 @@ import com.sluv.domain.closet.entity.Closet; import com.sluv.domain.closet.service.ClosetDomainService; import com.sluv.domain.comment.service.CommentDomainService; -import com.sluv.domain.item.dto.ItemSimpleDto; -import com.sluv.domain.item.entity.Item; -import com.sluv.domain.item.entity.ItemImg; -import com.sluv.domain.item.service.ItemDomainService; -import com.sluv.domain.item.service.ItemImgDomainService; -import com.sluv.domain.item.service.ItemScrapDomainService; -import com.sluv.domain.question.dto.QuestionImgSimpleDto; import com.sluv.domain.question.dto.QuestionSimpleResDto; import com.sluv.domain.question.entity.*; import com.sluv.domain.question.enums.QuestionStatus; -import com.sluv.domain.question.exception.QuestionReportDuplicateException; -import com.sluv.domain.question.exception.QuestionTypeNotFoundException; import com.sluv.domain.question.service.*; import com.sluv.domain.user.entity.User; -import com.sluv.domain.user.service.UserBlockDomainService; import com.sluv.domain.user.service.UserDomainService; -import com.sluv.infra.alarm.service.QuestionAlarmService; -import com.sluv.infra.counter.view.ViewCounter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.*; +import java.util.List; +import java.util.function.Function; @Service @Slf4j @RequiredArgsConstructor public class QuestionService { private final QuestionDomainService questionDomainService; - private final QuestionImgDomainService questionImgDomainService; - private final QuestionItemDomainService questionItemDomainService; private final QuestionRecommendCategoryDomainService questionRecommendCategoryDomainService; private final QuestionLikeDomainService questionLikeDomainService; - private final QuestionReportDomainService questionReportDomainService; private final CommentDomainService commentDomainService; - private final ItemDomainService itemDomainService; - private final ItemImgDomainService itemImgDomainService; private final CelebDomainService celebDomainService; private final NewCelebDomainService newCelebDomainService; private final RecentQuestionDomainService recentQuestionDomainService; - private final ItemScrapDomainService itemScrapDomainService; private final ClosetDomainService closetDomainService; private final QuestionVoteDomainService questionVoteDomainService; private final UserDomainService userDomainService; - private final UserBlockDomainService userBlockDomainService; - private final ViewCounter viewCounter; - private final QuestionAlarmService questionAlarmService; + private final QuestionImageManager questionImageManager; + private final QuestionItemManager questionItemManager; + private final QuestionVoteService questionVoteService; + private final QuestionModerationService questionModerationService; + private final QuestionModerationStatusService questionModerationStatusService; @Transactional @@ -85,17 +70,20 @@ public QuestionPostResDto postQuestionFind(Long userId, QuestionFindPostReqDto d newCeleb = newCelebDomainService.findByNewCelebIdOrNull(dto.getNewCelebId()); } + QuestionStatus questionStatus = getQuestionStatus(dto.getId()); QuestionFind questionFind = QuestionFind.toEntity(user, dto.getId(), dto.getTitle(), dto.getContent(), celeb, - newCeleb); + newCeleb, questionStatus); // 2. QuestionFind 저장 QuestionFind newQuestionFind = (QuestionFind) questionDomainService.saveQuestion(questionFind); // 3. QuestionImg 저장 - postQuestionImgs(dto.getImgList(), newQuestionFind); + questionImageManager.saveImages(dto.getImgList(), newQuestionFind); // 4. QuestionItem 저장 - postQuestionItems(dto.getItemList(), newQuestionFind); + questionItemManager.saveItems(dto.getItemList(), newQuestionFind); + + questionModerationService.createQuestionJobIfEnabled(newQuestionFind); return QuestionPostResDto.of(newQuestionFind.getId()); @@ -114,16 +102,20 @@ public QuestionPostResDto postQuestionBuy(Long userId, QuestionBuyPostReqDto dto user.getId(), dto.getId() == null ? null : dto.getId(), dto.getTitle()); // 1. 생성 or 수정 - QuestionBuy questionBuy = QuestionBuy.toEntity(user, dto.getId(), dto.getTitle(), dto.getVoteEndTime()); + QuestionStatus questionStatus = getQuestionStatus(dto.getId()); + QuestionBuy questionBuy = QuestionBuy.toEntity(user, dto.getId(), dto.getTitle(), dto.getVoteEndTime(), + questionStatus); // 2. QuestionBuy 저장 QuestionBuy newQuestionBuy = (QuestionBuy) questionDomainService.saveQuestion(questionBuy); // 3. QuestionImg 저장 - postQuestionImgs(dto.getImgList(), newQuestionBuy); + questionImageManager.saveImages(dto.getImgList(), newQuestionBuy); // 4. QuestionItem 저장 - postQuestionItems(dto.getItemList(), newQuestionBuy); + questionItemManager.saveItems(dto.getItemList(), newQuestionBuy); + + questionModerationService.createQuestionJobIfEnabled(newQuestionBuy); return QuestionPostResDto.of(newQuestionBuy.getId()); } @@ -141,17 +133,20 @@ public QuestionPostResDto postQuestionHowabout(Long userId, QuestionHowaboutPost user.getId(), dto.getId() == null ? null : dto.getId(), dto.getTitle()); // 1. 생성 or 수정 + QuestionStatus questionStatus = getQuestionStatus(dto.getId()); QuestionHowabout questionHowabout = QuestionHowabout.toEntity(user, dto.getId(), dto.getTitle(), - dto.getContent()); + dto.getContent(), questionStatus); // 2. QuestionHotabout 저장 QuestionHowabout newQuestionHowabout = (QuestionHowabout) questionDomainService.saveQuestion(questionHowabout); // 3. QuestionImg 저장 - postQuestionImgs(dto.getImgList(), newQuestionHowabout); + questionImageManager.saveImages(dto.getImgList(), newQuestionHowabout); // 4. QuestionItem 저장 - postQuestionItems(dto.getItemList(), newQuestionHowabout); + questionItemManager.saveItems(dto.getItemList(), newQuestionHowabout); + + questionModerationService.createQuestionJobIfEnabled(newQuestionHowabout); return QuestionPostResDto.of(newQuestionHowabout.getId()); } @@ -169,8 +164,9 @@ public QuestionPostResDto postQuestionRecommend(Long userId, QuestionRecommendPo log.info("추천해줘 게시글 등록 or 수정 - 사용자 : {}, 질문 게시글 : {}, 질문 게시글 제목 : {}", user.getId(), dto.getId() == null ? null : dto.getId(), dto.getTitle()); // 1. 생성 or 수정 + QuestionStatus questionStatus = getQuestionStatus(dto.getId()); QuestionRecommend questionRecommend = QuestionRecommend.toEntity(user, dto.getId(), dto.getTitle(), - dto.getContent()); + dto.getContent(), questionStatus); // 2. QuestionRecommend 저장 QuestionRecommend newQuestionRecommend = (QuestionRecommend) questionDomainService.saveQuestion( @@ -188,52 +184,16 @@ public QuestionPostResDto postQuestionRecommend(Long userId, QuestionRecommendPo questionRecommendCategoryDomainService.saveAll(recommendCategories); // 4. QuestionImg 저장 + questionImageManager.saveImages(dto.getImgList(), newQuestionRecommend); - postQuestionImgs(dto.getImgList(), newQuestionRecommend); + // 5. QuestionItem 저장 + questionItemManager.saveItems(dto.getItemList(), newQuestionRecommend); - // 4. QuestionItem 저장 - postQuestionItems(dto.getItemList(), newQuestionRecommend); + questionModerationService.createQuestionJobIfEnabled(newQuestionRecommend); return QuestionPostResDto.of(newQuestionRecommend.getId()); } - /** - * Question Img 저장 메소드 - */ - private void postQuestionImgs(List dtoList, Question question) { - // Question에 대한 Img 초기화 - questionImgDomainService.deleteAllByQuestionId(question.getId()); - - if (dtoList != null) { - // Question Img들 추가 - List imgList = dtoList.stream() - .map(imgDto -> QuestionImg.toEntity(question, imgDto.getImgUrl(), imgDto.getDescription(), - imgDto.getRepresentFlag(), imgDto.getSortOrder())) - .toList(); - - questionImgDomainService.saveAll(imgList); - } - } - - /** - * Questim Item 저장 메소드 - */ - private void postQuestionItems(List dtoList, Question question) { - // Question에 대한 Item 초기화 - questionItemDomainService.deleteAllByQuestionId(question.getId()); - if (dtoList != null) { - // Question Item들 추가 - List items = dtoList.stream().map(itemDto -> { - Item item = itemDomainService.findById(itemDto.getItemId()); - return QuestionItem.toEntity(question, item, itemDto.getDescription(), itemDto.getRepresentFlag(), - itemDto.getSortOrder()); - } - ).toList(); - - questionItemDomainService.saveAll(items); - } - } - @Transactional public void deleteQuestion(Long questionId) { log.info("질문 게시글 삭제 - 질문 게시글 : {}", questionId); @@ -241,635 +201,133 @@ public void deleteQuestion(Long questionId) { question.changeQuestionStatus(QuestionStatus.DELETED); } - @Transactional - public void postQuestionLike(Long userId, Long questionId) { - User user = userDomainService.findById(userId); - log.info("질문 게시글 좋아요 - 사용자 : {}, 질문 게시글 : {}", user.getId(), questionId); - // 해당 유저의 Question 게시물 like 여부 검색 - Boolean likeStatus = questionLikeDomainService.existsByQuestionIdAndUserId(questionId, user.getId()); - Question question = questionDomainService.findById(questionId); - - if (likeStatus) { - // like가 있다면 삭제 - questionLikeDomainService.deleteByQuestionIdAndUserId(questionId, user.getId()); - } else { - // like가 없다면 등록 - questionLikeDomainService.saveQuestionLike(user, question); - questionAlarmService.sendAlarmAboutQuestionLike(user.getId(), question.getId()); - } - - } - - @Transactional - public void postQuestionReport(Long userId, Long questionId, QuestionReportReqDto dto) { - User user = userDomainService.findById(userId); - log.info("질문 게시글 신고 - 사용자 : {}, 질문 게시글 : {}, 사유 : {}", user.getId(), questionId, dto.getReason()); - Boolean reportExist = questionReportDomainService.existsByQuestionIdAndReporterId(questionId, user.getId()); - - if (!reportExist) { - // 신고 내역이 없다면 신고 등록. - Question question = questionDomainService.findById(questionId); - - questionReportDomainService.saveQuestionReport(user, question, dto.getReason(), dto.getContent()); - - } else { - // 신고 내역이 있다면 중복 신고 방지. - throw new QuestionReportDuplicateException(); - } - } - @Transactional public QuestionGetDetailResDto getQuestionDetail(Long nowUserId, Long questionId) { Question question = questionDomainService.findById(questionId); User nowUser = userDomainService.findById(nowUserId); - - // Question Type 분류 - String qType; - if (question != null) { - if (question instanceof QuestionFind) { - qType = "Find"; - } else if (question instanceof QuestionBuy) { - qType = "Buy"; - } else if (question instanceof QuestionHowabout) { - qType = "How"; - } else if (question instanceof QuestionRecommend) { - qType = "Recommend"; - } else { - qType = null; - } - } else { - qType = null; - } - - // 작성자 + String questionType = getQuestionTypeOrNull(question); User writer = userDomainService.findByIdOrNull(question.getUser().getId()); + List closets = getClosets(nowUser); - // Question img List - List questionImgList = questionImgDomainService.findAllByQuestionId(questionId). - stream() - .map(questionImg -> { - QuestionVoteDataDto voteDataDto = null; - // QuestionBuy 라면 - if (qType != null && qType.equals("Buy")) { - voteDataDto = getVoteData(questionId, (long) questionImg.getSortOrder()); - } - - return QuestionImgResDto.of(questionImg, voteDataDto); - } - ).toList(); - - List closets; - - if (nowUser != null) { - closets = closetDomainService.findAllByUserId(nowUser.getId()); - } else { - closets = new ArrayList<>(); - } - - // Question Item List - List questionItemList = questionItemDomainService.findAllByQuestionId(questionId) - .stream() - .map(questionItem -> { - ItemSimpleDto itemSimpleDto = ItemSimpleDto.of( - questionItem.getItem(), - itemImgDomainService.findMainImg(questionItem.getItem().getId()), - itemScrapDomainService.getItemScrapStatus(questionItem.getItem(), closets) - ); - QuestionVoteDataDto questionVoteDataDto = null; - // QuestionBuy일 경우 투표수 추가. - if (qType != null & qType.equals("Buy")) { - questionVoteDataDto = getVoteData(questionId, (long) questionItem.getSortOrder()); - } - - return QuestionItemResDto.of(questionItem, itemSimpleDto, questionVoteDataDto); - }).toList(); - - // Question Like Num Count + List questionImages = questionImageManager.getQuestionImageResponses( + questionId, + getVoteDataResolver(questionId, questionType) + ); + List questionItems = questionItemManager.getQuestionItemResponses( + questionId, + closets, + getVoteDataResolver(questionId, questionType) + ); Long questionLikeNum = questionLikeDomainService.countByQuestionId(questionId); - - // Question Comment Num Count Long questionCommentNum = commentDomainService.countByQuestionId(questionId); + Boolean currentUserLike = hasCurrentUserLike(nowUser, questionId); + QuestionDetailTypeData typeData = getQuestionDetailTypeData(question, questionType, nowUser); - // hasLike 검색 - Boolean currentUserLike = - nowUser != null && questionLikeDomainService.existsByQuestionIdAndUserId(questionId, nowUser.getId()); - - CelebChipResponse celeb = null; - CelebChipResponse newCeleb = null; - LocalDateTime voteEndTime = null; - Long totalVoteNum = null; - Long voteStatus = null; - List recommendCategoryList = null; - - switch (qType) { - case "Find" -> { - QuestionFind questionFind = (QuestionFind) question; - if (questionFind.getCeleb() != null) { - celeb = CelebChipResponse.of(questionFind.getCeleb()); - } else { - newCeleb = CelebChipResponse.of(questionFind.getNewCeleb()); - } - } - case "Buy" -> { - QuestionBuy questionBuy = (QuestionBuy) question; - QuestionVote questionVote = - nowUser != null ? - questionVoteDomainService.findByQuestionIdAndUserIdOrNull(questionId, nowUser.getId()) - : null; - voteEndTime = questionBuy.getVoteEndTime(); - totalVoteNum = questionVoteDomainService.countByQuestionId(questionId); - voteStatus = questionVote != null - ? questionVote.getVoteSortOrder() - : null; - } - case "Recommend" -> - recommendCategoryList = questionRecommendCategoryDomainService.findAllByQuestionId(questionId) - .stream().map(QuestionRecommendCategory::getName).toList(); - } - - // RecentQuestion 등록 if (nowUser != null) { - recentQuestionDomainService.saveRecentQuestion(nowUser, qType, question); - - // SearchNum 증가 - increaseQuestionViewNum(nowUser.getId(), question); + recentQuestionDomainService.saveRecentQuestion(nowUser, questionType, question); + increaseQuestionViewNum(question); } return QuestionGetDetailResDto.of( - question, qType, writer, - questionImgList, questionItemList, + question, questionType, writer, + questionImages, questionItems, questionLikeNum, questionCommentNum, currentUserLike, writer != null && nowUser != null && nowUser.getId().equals(writer.getId()), - celeb, newCeleb, - voteEndTime, totalVoteNum, voteStatus, - recommendCategoryList + typeData.celeb(), typeData.newCeleb(), + typeData.voteEndTime(), typeData.totalVoteNum(), typeData.voteStatus(), + typeData.recommendCategories() ); } - private void increaseQuestionViewNum(Long userId, Question question) { - question.increaseSearchNum(); - } - - /** - * builder에 VoteNum, VotePercent 탑재 - */ - private QuestionVoteDataDto getVoteData(Long questionId, Long sortOrder) { - List questionVotes = questionVoteDomainService.findAllByQuestionId(questionId); - - // 해당 SortOrder의 투표 수 - Long voteNum = 0L; - for (QuestionVote questionVote : questionVotes) { - if (Objects.equals(questionVote.getVoteSortOrder(), sortOrder)) { - voteNum++; - } + private List getClosets(User user) { + if (user == null) { + return List.of(); } - return QuestionVoteDataDto.of( - voteNum, - questionVotes.size() != 0 - ? getVotePercent(voteNum, questionVotes.stream().count()) - : 0 - ); - } - - /** - * QuestionVote 퍼센트 계산. - */ - private Double getVotePercent(Long voteNum, Long totalVoteNum) { - double div = (double) voteNum / (double) totalVoteNum; - return Math.round(div * 1000) / 10.0; - } - - /** - * QuestionBuy 등록 및 취소 - */ - @Transactional - public void postQuestionVote(Long userId, Long questionId, QuestionVoteReqDto dto) { - User user = userDomainService.findById(userId); - log.info("질문 게시글 투표 - 사용자 : {}, 질문 게시글 : {}, 투표 : {}", user.getId(), questionId, dto.getVoteSortOrder()); - QuestionVote questionVote = questionVoteDomainService.findByQuestionIdAndUserIdOrNull(questionId, user.getId()); - - if (questionVote == null) { - // 투표 등록 - - // Question 검색 - Question question = questionDomainService.findById(questionId); - - // QuestionVote 생성 및 저장 - questionVoteDomainService.saveQuestionVote(question, user, dto.getVoteSortOrder()); - - } else { - // 투표 취소 - - // 해당 QuestionVote 삭제. - questionVoteDomainService.deleteById(questionVote.getId()); - } + return closetDomainService.findAllByUserId(user.getId()); } - /** - * Question 상세보기 하단의 추천 게시글 - */ - @Transactional(readOnly = true) - public List getWaitQuestionBuy(Long userId, Long questionId) { - User user = userDomainService.findById(userId); - List blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - - List interestedCelebs = new ArrayList<>(); - if (user != null) { - interestedCelebs = celebDomainService.findInterestedCeleb(user); + private QuestionStatus getQuestionStatus(Long questionId) { + if (questionId == null) { + return questionModerationStatusService.getInitialQuestionStatus(); } - return questionDomainService.getWaitQuestionBuy(user, questionId, interestedCelebs, blockUserIds) - .stream() - .map(questionBuy -> getQuestionSimpleResDto(questionBuy, "Buy")) - .toList(); - } - - /** - * Wait QuestionRecommend 조회 - */ - @Transactional(readOnly = true) - public List getWaitQuestionRecommend(Long userId, Long questionId) { - User user = userDomainService.findById(userId); - List blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - - return questionDomainService.getWaitQuestionRecommend(user, questionId, blockUserIds) - .stream() - .map(questionRecommend -> getQuestionSimpleResDto(questionRecommend, "Recommend")) - .toList(); + return questionModerationStatusService.getUpdateQuestionStatus(); } - /** - * 가 - * Wait QuestionHowabout 조회 - */ - @Transactional(readOnly = true) - public List getWaitQuestionHowabout(Long userId, Long questionId) { - User user = userDomainService.findById(userId); - List blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - - return questionDomainService.getWaitQuestionHowabout(user, questionId, blockUserIds) - .stream() - .map(questionHowabout -> getQuestionSimpleResDto(questionHowabout, "How")) - .toList(); - } - - /** - * Wait QuestionFind 조회 - */ - @Transactional(readOnly = true) - public List getWaitQuestionFind(Long userId, Long questionId) { - User user = userDomainService.findById(userId); - List blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - - List interestedCelebs = new ArrayList<>(); - - if (user != null) { - interestedCelebs = celebDomainService.findInterestedCeleb(user); - } - - return questionDomainService.getWaitQuestionFind(user, questionId, interestedCelebs, blockUserIds) - .stream() - .map(questionFind -> getQuestionSimpleResDto(questionFind, "Find")) - .toList(); - } - - public QuestionSimpleResDto getQuestionSimpleResDto(Question question, String qType) { - List imgList = new ArrayList<>(); - List itemImgList = new ArrayList<>(); - String celebName = null; - List categoryNameList = null; - -// if (!qType.equals("Buy")) { - if (!(question instanceof QuestionBuy)) { - // 이미지 URL - QuestionImg questionImg = questionImgDomainService.findByQuestionIdAndRepresentFlag(question.getId(), true); - - // 이미지 Dto로 변경 - if (questionImg != null) { - imgList.add(QuestionImgSimpleDto.of(questionImg)); - } - - // 아이템 이미지 URL - QuestionItem questionItem = questionItemDomainService.findByQuestionIdAndRepresentFlag(question.getId(), - true); - - // 아이템 이미지 Dto로 변경 - if (questionItem != null) { - ItemImg mainImg = itemImgDomainService.findMainImg(questionItem.getItem().getId()); - imgList.add(QuestionImgSimpleDto.of(mainImg)); - } - - } else { - // 이미지 URL - imgList = questionImgDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(QuestionImgSimpleDto::of).toList(); - - // 아이템 이미지 URL - itemImgList = questionItemDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(item -> { - ItemImg mainImg = itemImgDomainService.findMainImg(item.getItem().getId()); - return QuestionImgSimpleDto.of(mainImg); - }).toList(); - } - -// if (qType.equals("Recommend")) { - if (question instanceof QuestionRecommend) { - categoryNameList = questionRecommendCategoryDomainService.findAllByQuestionId(question.getId()).stream() - .map(QuestionRecommendCategory::getName).toList(); + private Function getVoteDataResolver(Long questionId, String questionType) { + if (!"Buy".equals(questionType)) { + return sortOrder -> null; } - // Question 좋아요 수 - Long likeNum = questionLikeDomainService.countByQuestionId(question.getId()); - - // Question 댓글 수 - Long commentNum = commentDomainService.countByQuestionId(question.getId()); - - User writer = userDomainService.findByIdOrNull(question.getUser().getId()); - - return QuestionSimpleResDto.of(question, writer, likeNum, commentNum, - imgList, itemImgList, categoryNameList); + return sortOrder -> questionVoteService.getVoteData(questionId, sortOrder.longValue()); } - /** - * Question 리스트를 최신순으로 조회 - */ - @Transactional(readOnly = true) - public PaginationResponse getTotalQuestionList(Long userId, Pageable pageable) { - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - } - - Page questionPage = questionDomainService.getTotalQuestionList(blockUserIds, pageable); - List content = questionPage.stream().map(question -> { - String qType; - if (question instanceof QuestionBuy) { - qType = "Buy"; - } else if (question instanceof QuestionFind) { - qType = "Find"; - } else if (question instanceof QuestionHowabout) { - qType = "Howabout"; - } else if (question instanceof QuestionRecommend) { - qType = "Recommend"; - } else { - throw new QuestionTypeNotFoundException(); - } - return getQuestionSimpleResDto(question, qType); - }).toList(); - - return PaginationResponse.of(questionPage, content); + private Boolean hasCurrentUserLike(User nowUser, Long questionId) { + return nowUser != null && questionLikeDomainService.existsByQuestionIdAndUserId(questionId, nowUser.getId()); } - @Transactional(readOnly = true) - public PaginationResponse getQuestionBuyList(Long userId, String voteStatus, - Pageable pageable) { - User user = userDomainService.findById(userId); - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - } - - Page questionPage = questionDomainService.getQuestionBuyList(voteStatus, blockUserIds, pageable); - - List content = questionPage.stream().map(question -> { - - List imgList = questionImgDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(questionImg -> { - QuestionVoteDataDto voteDataDto = getVoteData(questionImg.getQuestion().getId(), - (long) questionImg.getSortOrder()); - - return QuestionImgResDto.of(questionImg, voteDataDto); - }).toList(); - - // 아이템 이미지 URL - List itemImgList = questionItemDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(questionItem -> { - ItemSimpleDto itemSimpleDto = ItemSimpleDto.of( - questionItem.getItem(), - itemImgDomainService.findMainImg(questionItem.getItem().getId()), - null - ); - QuestionVoteDataDto questionVoteDataDto = getVoteData(questionItem.getQuestion().getId(), - (long) questionItem.getSortOrder()); - return QuestionItemResDto.of(questionItem, itemSimpleDto, questionVoteDataDto); - }).toList(); - - Long voteCount = getTotalVoteCount(imgList, itemImgList); - - QuestionVote questionVote = null; - if (user != null) { - questionVote = questionVoteDomainService.findByQuestionIdAndUserIdOrNull(question.getId(), - user.getId()); - } - - User writer = userDomainService.findByIdOrNull(question.getUser().getId()); - - return QuestionBuySimpleResDto.of(user, question, writer, voteCount, imgList, itemImgList, - question.getVoteEndTime(), - questionVote); - }).toList(); - - return PaginationResponse.of(questionPage, content); + private QuestionDetailTypeData getQuestionDetailTypeData(Question question, String questionType, User nowUser) { + return switch (questionType) { + case "Find" -> getFindQuestionDetailData((QuestionFind) question); + case "Buy" -> getBuyQuestionDetailData((QuestionBuy) question, nowUser); + case "Recommend" -> getRecommendQuestionDetailData(question.getId()); + default -> QuestionDetailTypeData.empty(); + }; } - private Long getTotalVoteCount(List imgList, List itemImgList) { - long totalVoteCount = 0L; - - for (QuestionImgResDto questionImgResDto : imgList) { - totalVoteCount += questionImgResDto.getVoteNum(); - } - - for (QuestionItemResDto questionItemResDto : itemImgList) { - totalVoteCount += questionItemResDto.getVoteNum(); - } - - return totalVoteCount; - } + private QuestionDetailTypeData getFindQuestionDetailData(QuestionFind question) { + CelebChipResponse celeb = null; + CelebChipResponse newCeleb = null; - /** - * QuestionFind 커뮤니티 게시글 조회. - */ - @Transactional(readOnly = true) - public PaginationResponse getQuestionFindList(Long userId, Long celebId, Boolean isNewCeleb, - Pageable pageable) { - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); + if (question.getCeleb() != null) { + celeb = CelebChipResponse.of(question.getCeleb()); + } else { + newCeleb = CelebChipResponse.of(question.getNewCeleb()); } - Page questionPage = questionDomainService.getQuestionFindList(celebId, isNewCeleb, blockUserIds, pageable); - - List content = questionPage.stream().map(question -> - getQuestionSimpleResDto(question, "Find") - ).toList(); - - return PaginationResponse.of(questionPage, content); + return QuestionDetailTypeData.ofCeleb(celeb, newCeleb); } - @Transactional(readOnly = true) - public PaginationResponse getQuestionHowaboutList(Long userId, Pageable pageable) { - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - } - - Page questionPage = questionDomainService.getQuestionHowaboutList(blockUserIds, pageable); - List content = questionPage.stream().map(question -> - getQuestionSimpleResDto(question, "How") - ).toList(); - - return PaginationResponse.of(questionPage, content); + private QuestionDetailTypeData getBuyQuestionDetailData(QuestionBuy question, User nowUser) { + QuestionVote questionVote = nowUser != null + ? questionVoteDomainService.findByQuestionIdAndUserIdOrNull(question.getId(), nowUser.getId()) + : null; + Long voteStatus = questionVote != null + ? questionVote.getVoteSortOrder() + : null; + + return QuestionDetailTypeData.ofVote( + question.getVoteEndTime(), + questionVoteDomainService.countByQuestionId(question.getId()), + voteStatus + ); } - @Transactional(readOnly = true) - public PaginationResponse getQuestionRecommendList(Long userId, String hashtag, Pageable pageable) { - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - } - - Page questionPage = questionDomainService.getQuestionRecommendList(hashtag, blockUserIds, pageable); - List content = questionPage.stream().map(question -> - getQuestionSimpleResDto(question, "Recommend") - ).toList(); + private QuestionDetailTypeData getRecommendQuestionDetailData(Long questionId) { + List recommendCategories = questionRecommendCategoryDomainService.findAllByQuestionId(questionId) + .stream() + .map(QuestionRecommendCategory::getName) + .toList(); - return PaginationResponse.of(questionPage, content); + return QuestionDetailTypeData.ofRecommendCategories(recommendCategories); } - private String getQuestionCelebName(QuestionFind questionFind) { - return questionFind.getCeleb() != null - ? questionFind.getCeleb().getParent() != null - ? questionFind.getCeleb().getParent().getCelebNameKr() + " " + questionFind.getCeleb().getCelebNameKr() - : questionFind.getCeleb().getCelebNameKr() - : questionFind.getNewCeleb().getCelebName(); + private void increaseQuestionViewNum(Question question) { + question.increaseSearchNum(); } - /** - * 일일 Hot Question 조회 기능. - */ - @Transactional(readOnly = true) - public List getDailyHotQuestionList(Long userId) { - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); + private String getQuestionTypeOrNull(Question question) { + if (question instanceof QuestionFind) { + return "Find"; + } else if (question instanceof QuestionBuy) { + return "Buy"; + } else if (question instanceof QuestionHowabout) { + return "How"; + } else if (question instanceof QuestionRecommend) { + return "Recommend"; } - - List dailyHoyQuestionList = questionDomainService.getDailyHotQuestion(blockUserIds); - - List result = dailyHoyQuestionList.stream().map(question -> { - List questionImgSimpleList = getQuestionImgSimpleList(question); - User writer = userDomainService.findByIdOrNull(question.getUser().getId()); - return QuestionHomeResDto.of(question, writer, questionImgSimpleList); - - }).toList(); - - return result; - + return null; } - private List getQuestionImgSimpleList(Question question) { - - // Question이 QuestionBuy인 경우 모든 이미지를 순서대로 조회 - if (question instanceof QuestionBuy) { - List result = new ArrayList<>(); - - List questionImgList = questionImgDomainService.findAllByQuestionId(question.getId()); - List questionItemImgList = questionItemDomainService.findAllByQuestionId(question.getId()).stream() - .map(questionItem -> itemImgDomainService.findMainImg(questionItem.getItem().getId())).toList(); - - questionImgList.forEach(questionImg -> result.add(QuestionImgSimpleDto.of(questionImg))); - questionItemImgList.forEach(itemImg -> result.add(QuestionImgSimpleDto.of(itemImg))); - - result.sort(Comparator.comparing(QuestionImgSimpleDto::getSortOrder)); - - return result; - } else {// 그 외에는 대표이미지만 조화 - - QuestionImg questionImg = questionImgDomainService.findByQuestionIdAndRepresentFlag(question.getId(), true); - QuestionItem questionItem = questionItemDomainService.findByQuestionIdAndRepresentFlag(question.getId(), - true); - - String imgUrl = null; - - if (questionImg != null) { - imgUrl = questionImg.getImgUrl(); - } else if (questionItem != null) { - imgUrl = itemImgDomainService.findMainImg(questionItem.getItem().getId()).getItemImgUrl(); - } - - return imgUrl != null - ? Arrays.asList(new QuestionImgSimpleDto(imgUrl, 0L)) - : null; - } - } - - /** - * 주간 Hot Question 조회 기능. - */ - @Transactional(readOnly = true) - public PaginationResponse getWeeklyHotQuestionList(Long userId, Pageable pageable) { - List blockUserIds = new ArrayList<>(); - if (userId != null) { - blockUserIds = userBlockDomainService.getAllBlockedUser(userId).stream() - .map(userBlock -> userBlock.getBlockedUser().getId()) - .toList(); - } - - Page page = questionDomainService.getWeeklyHotQuestion(blockUserIds, pageable); - - List content = page.stream().map(question -> { - List categoryList = null; - if (question instanceof QuestionRecommend) { - categoryList = questionRecommendCategoryDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(QuestionRecommendCategory::getName).toList(); - } - - List imgList = questionImgDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(QuestionImgSimpleDto::of) - .toList(); - - List itemImgList = questionItemDomainService.findAllByQuestionId(question.getId()) - .stream() - .map(questionItem -> - QuestionImgSimpleDto.of(itemImgDomainService.findMainImg(questionItem.getItem().getId())) - ) - .toList(); - - Long commentNum = commentDomainService.countByQuestionId(question.getId()); - Long likeNum = questionLikeDomainService.countByQuestionId(question.getId()); - User writer = userDomainService.findByIdOrNull(question.getUser().getId()); - - return QuestionSimpleResDto.of(question, writer, likeNum, commentNum, imgList, itemImgList, categoryList); - }).toList(); - - return PaginationResponse.of(page, content); - } } - diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionVoteService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionVoteService.java new file mode 100644 index 00000000..2259ffcc --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionVoteService.java @@ -0,0 +1,72 @@ +package com.sluv.api.question.service; + +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.api.question.dto.QuestionImgResDto; +import com.sluv.api.question.dto.QuestionItemResDto; +import com.sluv.api.question.dto.QuestionVoteReqDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.entity.QuestionVote; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionVoteDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +@Service +@Slf4j +@RequiredArgsConstructor +public class QuestionVoteService { + + private final QuestionDomainService questionDomainService; + private final QuestionVoteDomainService questionVoteDomainService; + private final UserDomainService userDomainService; + + @Transactional + public void postQuestionVote(Long userId, Long questionId, QuestionVoteReqDto dto) { + User user = userDomainService.findById(userId); + log.info("질문 게시글 투표 - 사용자 : {}, 질문 게시글 : {}, 투표 : {}", user.getId(), questionId, dto.getVoteSortOrder()); + QuestionVote questionVote = questionVoteDomainService.findByQuestionIdAndUserIdOrNull(questionId, user.getId()); + + if (questionVote == null) { + Question question = questionDomainService.findById(questionId); + questionVoteDomainService.saveQuestionVote(question, user, dto.getVoteSortOrder()); + } else { + questionVoteDomainService.deleteById(questionVote.getId()); + } + } + + @Transactional(readOnly = true) + public QuestionVoteDataDto getVoteData(Long questionId, Long sortOrder) { + List questionVotes = questionVoteDomainService.findAllByQuestionId(questionId); + + long voteNum = questionVotes.stream() + .filter(questionVote -> Objects.equals(questionVote.getVoteSortOrder(), sortOrder)) + .count(); + + return QuestionVoteDataDto.of( + voteNum, + questionVotes.isEmpty() ? 0.0 : getVotePercent(voteNum, (long) questionVotes.size()) + ); + } + + public Long getTotalVoteCount(List questionImages, List questionItems) { + long imageVoteCount = questionImages.stream() + .mapToLong(QuestionImgResDto::getVoteNum) + .sum(); + long itemVoteCount = questionItems.stream() + .mapToLong(QuestionItemResDto::getVoteNum) + .sum(); + return imageVoteCount + itemVoteCount; + } + + private Double getVotePercent(Long voteNum, Long totalVoteNum) { + double div = (double) voteNum / (double) totalVoteNum; + return Math.round(div * 1000) / 10.0; + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/question/service/QuestionWaitService.java b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionWaitService.java new file mode 100644 index 00000000..338cd990 --- /dev/null +++ b/sluv-api/src/main/java/com/sluv/api/question/service/QuestionWaitService.java @@ -0,0 +1,81 @@ +package com.sluv.api.question.service; + +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.domain.celeb.entity.Celeb; +import com.sluv.domain.celeb.service.CelebDomainService; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserBlockDomainService; +import com.sluv.domain.user.service.UserDomainService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class QuestionWaitService { + + private final QuestionDomainService questionDomainService; + private final UserDomainService userDomainService; + private final UserBlockDomainService userBlockDomainService; + private final CelebDomainService celebDomainService; + private final QuestionResponseAssembler questionResponseAssembler; + + @Transactional(readOnly = true) + public List getWaitQuestionBuy(Long userId, Long questionId) { + User user = userDomainService.findById(userId); + List blockedUserIds = getBlockedUserIds(userId); + List interestedCelebs = getInterestedCelebs(user); + + return questionDomainService.getWaitQuestionBuy(user, questionId, interestedCelebs, blockedUserIds).stream() + .map(questionResponseAssembler::getQuestionSimpleResponseWithMainImage) + .toList(); + } + + @Transactional(readOnly = true) + public List getWaitQuestionFind(Long userId, Long questionId) { + User user = userDomainService.findById(userId); + List blockedUserIds = getBlockedUserIds(userId); + List interestedCelebs = getInterestedCelebs(user); + + return questionDomainService.getWaitQuestionFind(user, questionId, interestedCelebs, blockedUserIds).stream() + .map(questionResponseAssembler::getQuestionSimpleResponseWithMainImage) + .toList(); + } + + @Transactional(readOnly = true) + public List getWaitQuestionHowabout(Long userId, Long questionId) { + User user = userDomainService.findById(userId); + List blockedUserIds = getBlockedUserIds(userId); + + return questionDomainService.getWaitQuestionHowabout(user, questionId, blockedUserIds).stream() + .map(questionResponseAssembler::getQuestionSimpleResponseWithMainImage) + .toList(); + } + + @Transactional(readOnly = true) + public List getWaitQuestionRecommend(Long userId, Long questionId) { + User user = userDomainService.findById(userId); + List blockedUserIds = getBlockedUserIds(userId); + + return questionDomainService.getWaitQuestionRecommend(user, questionId, blockedUserIds).stream() + .map(questionResponseAssembler::getQuestionSimpleResponseWithMainImage) + .toList(); + } + + private List getBlockedUserIds(Long userId) { + return userBlockDomainService.getAllBlockedUser(userId).stream() + .map(userBlock -> userBlock.getBlockedUser().getId()) + .toList(); + } + + private List getInterestedCelebs(User user) { + if (user == null) { + return List.of(); + } + + return celebDomainService.findInterestedCeleb(user); + } +} diff --git a/sluv-api/src/main/java/com/sluv/api/search/controller/SearchController.java b/sluv-api/src/main/java/com/sluv/api/search/controller/SearchController.java index 744aa9de..86edef0d 100644 --- a/sluv-api/src/main/java/com/sluv/api/search/controller/SearchController.java +++ b/sluv-api/src/main/java/com/sluv/api/search/controller/SearchController.java @@ -16,7 +16,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; import org.springframework.web.bind.annotation.*; import java.util.List; diff --git a/sluv-api/src/main/java/com/sluv/api/search/service/SearchEngineService.java b/sluv-api/src/main/java/com/sluv/api/search/service/SearchEngineService.java index 39362476..628673f3 100644 --- a/sluv-api/src/main/java/com/sluv/api/search/service/SearchEngineService.java +++ b/sluv-api/src/main/java/com/sluv/api/search/service/SearchEngineService.java @@ -1,7 +1,7 @@ package com.sluv.api.search.service; import com.sluv.api.common.response.PaginationResponse; -import com.sluv.api.question.service.QuestionService; +import com.sluv.api.question.helper.QuestionResponseAssembler; import com.sluv.api.search.dto.SearchItemCountResDto; import com.sluv.api.search.engine.SearchEngine; import com.sluv.domain.item.dto.ItemSimpleDto; @@ -34,7 +34,7 @@ public class SearchEngineService { private final SearchEngine searchEngine; private final SearchService searchService; - private final QuestionService questionService; + private final QuestionResponseAssembler questionResponseAssembler; private final ItemDomainService itemDomainService; private final UserDomainService userDomainService; @@ -92,7 +92,7 @@ public CompletableFuture> getSearchQues List content = searchQuestionPage.stream() .map(question -> - questionService.getQuestionSimpleResDto((Question) question, qType) + questionResponseAssembler.getQuestionSimpleResponseWithMainImage((Question) question) ).toList(); // 최근 검색 등록 diff --git a/sluv-api/src/main/java/com/sluv/api/user/service/UserLikeService.java b/sluv-api/src/main/java/com/sluv/api/user/service/UserLikeService.java index f86c207b..3a49a93e 100644 --- a/sluv-api/src/main/java/com/sluv/api/user/service/UserLikeService.java +++ b/sluv-api/src/main/java/com/sluv/api/user/service/UserLikeService.java @@ -2,7 +2,7 @@ import com.sluv.api.comment.dto.reponse.CommentSimpleResponse; import com.sluv.api.common.response.PaginationCountResponse; -import com.sluv.api.question.mapper.QuestionDtoMapper; +import com.sluv.api.question.helper.QuestionResponseAssembler; import com.sluv.domain.comment.entity.Comment; import com.sluv.domain.comment.service.CommentDomainService; import com.sluv.domain.item.dto.ItemSimpleDto; @@ -34,7 +34,7 @@ public class UserLikeService { private final QuestionDomainService questionDomainService; private final CommentDomainService commentDomainService; - private final QuestionDtoMapper questionDtoMapper; + private final QuestionResponseAssembler questionResponseAssembler; /** @@ -68,7 +68,8 @@ public PaginationCountResponse getUserLikeQuestion(Long us Page questionPage = questionDomainService.getUserLikeQuestion(user, blockUserIds, pageable); List content = questionPage.stream() - .map(questionDtoMapper::dtoBuildByQuestionType).toList(); + .map(questionResponseAssembler::getQuestionSimpleResponse) + .toList(); return new PaginationCountResponse<>(questionPage.hasNext(), questionPage.getNumber(), content, questionPage.getTotalElements()); diff --git a/sluv-api/src/main/java/com/sluv/api/user/service/UserService.java b/sluv-api/src/main/java/com/sluv/api/user/service/UserService.java index 503d5dff..3929b9ac 100644 --- a/sluv-api/src/main/java/com/sluv/api/user/service/UserService.java +++ b/sluv-api/src/main/java/com/sluv/api/user/service/UserService.java @@ -7,7 +7,7 @@ import com.sluv.api.item.helper.ItemHelper; import com.sluv.api.item.service.ItemCacheService; import com.sluv.api.item.service.TempItemService; -import com.sluv.api.question.mapper.QuestionDtoMapper; +import com.sluv.api.question.helper.QuestionResponseAssembler; import com.sluv.api.user.dto.*; import com.sluv.domain.closet.entity.Closet; import com.sluv.domain.closet.service.ClosetDomainService; @@ -61,7 +61,7 @@ public class UserService { private final UserBlockDomainService userBlockDomainService; private final ItemHelper itemHelper; - private final QuestionDtoMapper questionDtoMapper; + private final QuestionResponseAssembler questionResponseAssembler; private final ItemCacheService itemCacheService; private final WebHookService webHookService; private final UserWithdrawDataService userWithdrawDataService; @@ -195,7 +195,7 @@ public PaginationCountResponse getUserUploadQuestion(Long Page questionPage = questionDomainService.getUserAllQuestion(user, pageable); List content = questionPage.stream() - .map(questionDtoMapper::dtoBuildByQuestionType) + .map(questionResponseAssembler::getQuestionSimpleResponse) .toList(); return new PaginationCountResponse<>(questionPage.hasNext(), questionPage.getNumber(), content, diff --git a/sluv-api/src/test/java/com/sluv/api/domain/moderation/service/QuestionModerationServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/moderation/service/QuestionModerationServiceTest.java new file mode 100644 index 00000000..51ad7808 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/moderation/service/QuestionModerationServiceTest.java @@ -0,0 +1,79 @@ +package com.sluv.api.domain.moderation.service; + +import com.sluv.api.moderation.service.QuestionModerationService; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import com.sluv.domain.featureflag.service.FeatureFlagDomainService; +import com.sluv.domain.moderation.service.ModerationJobDomainService; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.user.entity.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionModerationServiceTest { + + @InjectMocks + private QuestionModerationService questionModerationService; + + @Mock + private FeatureFlagDomainService featureFlagDomainService; + + @Mock + private ModerationJobDomainService moderationJobDomainService; + + @Test + @DisplayName("MODERATION_JOB_CREATION이 true면 질문 검수 작업 생성을 요청한다.") + void createQuestionJobIfFeatureFlagEnabledTest() { + // given + User user = User.builder().id(10L).build(); + QuestionHowabout question = QuestionHowabout.builder() + .id(1L) + .user(user) + .title("질문") + .build(); + + when(featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION)) + .thenReturn(true); + + // when + questionModerationService.createQuestionJobIfEnabled(question); + + // then + verify(featureFlagDomainService).isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION); + verify(moderationJobDomainService).createQuestionJobIfAbsent(question.getId(), user.getId()); + } + + @Test + @DisplayName("MODERATION_JOB_CREATION이 false면 질문 검수 작업 생성을 요청하지 않는다.") + void doesNotCreateQuestionJobIfFeatureFlagDisabledTest() { + // given + User user = User.builder().id(10L).build(); + QuestionHowabout question = QuestionHowabout.builder() + .id(1L) + .user(user) + .title("질문") + .build(); + + when(featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION)) + .thenReturn(false); + + // when + questionModerationService.createQuestionJobIfEnabled(question); + + // then + verify(featureFlagDomainService).isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION); + verify(moderationJobDomainService, never()).createQuestionJobIfAbsent( + anyLong(), + anyLong() + ); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/moderation/service/QuestionModerationStatusServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/moderation/service/QuestionModerationStatusServiceTest.java new file mode 100644 index 00000000..6526bfa3 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/moderation/service/QuestionModerationStatusServiceTest.java @@ -0,0 +1,81 @@ +package com.sluv.api.domain.moderation.service; + +import com.sluv.api.moderation.service.QuestionModerationStatusService; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import com.sluv.domain.featureflag.service.FeatureFlagDomainService; +import com.sluv.domain.question.enums.QuestionStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionModerationStatusServiceTest { + + @InjectMocks + private QuestionModerationStatusService questionModerationStatusService; + + @Mock + private FeatureFlagDomainService featureFlagDomainService; + + @Test + @DisplayName("질문 생성 PENDING 플래그가 true면 생성 상태는 PENDING이다.") + void getInitialQuestionStatusWithPendingFlagEnabledTest() { + // given + when(featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_CREATE_PENDING)) + .thenReturn(true); + + // when + QuestionStatus status = questionModerationStatusService.getInitialQuestionStatus(); + + // then + assertThat(status).isEqualTo(QuestionStatus.PENDING); + } + + @Test + @DisplayName("질문 생성 PENDING 플래그가 false면 생성 상태는 ACTIVE다.") + void getInitialQuestionStatusWithPendingFlagDisabledTest() { + // given + when(featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_CREATE_PENDING)) + .thenReturn(false); + + // when + QuestionStatus status = questionModerationStatusService.getInitialQuestionStatus(); + + // then + assertThat(status).isEqualTo(QuestionStatus.ACTIVE); + } + + @Test + @DisplayName("질문 수정 PENDING 플래그가 true면 수정 상태는 PENDING이다.") + void getUpdateQuestionStatusWithPendingFlagEnabledTest() { + // given + when(featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_UPDATE_PENDING)) + .thenReturn(true); + + // when + QuestionStatus status = questionModerationStatusService.getUpdateQuestionStatus(); + + // then + assertThat(status).isEqualTo(QuestionStatus.PENDING); + } + + @Test + @DisplayName("질문 수정 PENDING 플래그가 false면 수정 상태는 ACTIVE다.") + void getUpdateQuestionStatusWithPendingFlagDisabledTest() { + // given + when(featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_UPDATE_PENDING)) + .thenReturn(false); + + // when + QuestionStatus status = questionModerationStatusService.getUpdateQuestionStatus(); + + // then + assertThat(status).isEqualTo(QuestionStatus.ACTIVE); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionImageManagerTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionImageManagerTest.java new file mode 100644 index 00000000..66f580a7 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionImageManagerTest.java @@ -0,0 +1,146 @@ +package com.sluv.api.domain.question.helper; + +import com.sluv.api.question.dto.QuestionImgReqDto; +import com.sluv.api.question.dto.QuestionImgResDto; +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.api.question.helper.QuestionImageManager; +import com.sluv.domain.item.repository.ItemImgRepository; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionImg; +import com.sluv.domain.question.repository.QuestionImgRepository; +import com.sluv.domain.question.repository.QuestionItemRepository; +import com.sluv.domain.question.service.QuestionImgDomainService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionImageManagerTest { + + @InjectMocks + private QuestionImageManager questionImageManager; + + @Mock + private QuestionImgDomainService questionImgDomainService; + + @Mock + private QuestionImgRepository questionImgRepository; + + @Mock + private QuestionItemRepository questionItemRepository; + + @Mock + private ItemImgRepository itemImgRepository; + + @Test + @DisplayName("질문 이미지 목록이 null이면 기존 이미지만 삭제한다.") + void saveImagesWithNullRequestTest() { + // given + QuestionHowabout question = createQuestion(1L); + + // when + questionImageManager.saveImages(null, question); + + // then + verify(questionImgDomainService).deleteAllByQuestionId(question.getId()); + verify(questionImgDomainService, never()).saveAll(org.mockito.ArgumentMatchers.anyList()); + } + + @Test + @DisplayName("질문 이미지 목록을 저장한다.") + void saveImagesTest() { + // given + QuestionHowabout question = createQuestion(1L); + List imageRequests = List.of( + QuestionImgReqDto.builder() + .imgUrl("https://image.test/1.jpg") + .description("대표 이미지") + .representFlag(true) + .sortOrder(1) + .build(), + QuestionImgReqDto.builder() + .imgUrl("https://image.test/2.jpg") + .description("추가 이미지") + .representFlag(false) + .sortOrder(2) + .build() + ); + + // when + questionImageManager.saveImages(imageRequests, question); + + // then + verify(questionImgDomainService).deleteAllByQuestionId(question.getId()); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(questionImgDomainService).saveAll(captor.capture()); + + List savedQuestionImages = captor.getValue(); + assertThat(savedQuestionImages).hasSize(2); + assertThat(savedQuestionImages.get(0).getQuestion()).isEqualTo(question); + assertThat(savedQuestionImages.get(0).getImgUrl()).isEqualTo("https://image.test/1.jpg"); + assertThat(savedQuestionImages.get(0).getDescription()).isEqualTo("대표 이미지"); + assertThat(savedQuestionImages.get(0).getRepresentFlag()).isTrue(); + assertThat(savedQuestionImages.get(0).getSortOrder()).isEqualTo(1); + assertThat(savedQuestionImages.get(1).getQuestion()).isEqualTo(question); + assertThat(savedQuestionImages.get(1).getImgUrl()).isEqualTo("https://image.test/2.jpg"); + assertThat(savedQuestionImages.get(1).getDescription()).isEqualTo("추가 이미지"); + assertThat(savedQuestionImages.get(1).getRepresentFlag()).isFalse(); + assertThat(savedQuestionImages.get(1).getSortOrder()).isEqualTo(2); + } + + @Test + @DisplayName("질문 이미지 응답에 투표 데이터를 포함한다.") + void getQuestionImageResponsesTest() { + // given + Long questionId = 1L; + QuestionHowabout question = createQuestion(questionId); + QuestionImg questionImg = QuestionImg.builder() + .question(question) + .imgUrl("https://question-image.test/detail.jpg") + .description("상세 이미지") + .sortOrder(2) + .representFlag(true) + .build(); + QuestionVoteDataDto voteData = QuestionVoteDataDto.builder() + .voteNum(3L) + .votePercent(75.0) + .build(); + + when(questionImgRepository.findAllByQuestionId(questionId)).thenReturn(List.of(questionImg)); + + // when + List responses = questionImageManager.getQuestionImageResponses( + questionId, + sortOrder -> voteData + ); + + // then + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getImgUrl()).isEqualTo("https://question-image.test/detail.jpg"); + assertThat(responses.get(0).getDescription()).isEqualTo("상세 이미지"); + assertThat(responses.get(0).getRepresentFlag()).isTrue(); + assertThat(responses.get(0).getSortOrder()).isEqualTo(2); + assertThat(responses.get(0).getVoteNum()).isEqualTo(3L); + assertThat(responses.get(0).getVotePercent()).isEqualTo(75.0); + } + + private QuestionHowabout createQuestion(Long id) { + return QuestionHowabout.builder() + .id(id) + .title("질문 제목") + .content("질문 내용") + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionItemManagerTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionItemManagerTest.java new file mode 100644 index 00000000..aa855147 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionItemManagerTest.java @@ -0,0 +1,185 @@ +package com.sluv.api.domain.question.helper; + +import com.sluv.api.question.dto.QuestionItemResDto; +import com.sluv.api.question.dto.QuestionItemReqDto; +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.api.question.helper.QuestionItemManager; +import com.sluv.domain.brand.entity.NewBrand; +import com.sluv.domain.celeb.entity.NewCeleb; +import com.sluv.domain.item.entity.Item; +import com.sluv.domain.item.entity.ItemImg; +import com.sluv.domain.item.service.ItemDomainService; +import com.sluv.domain.item.service.ItemImgDomainService; +import com.sluv.domain.item.service.ItemScrapDomainService; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionItem; +import com.sluv.domain.question.service.QuestionItemDomainService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionItemManagerTest { + + @InjectMocks + private QuestionItemManager questionItemManager; + + @Mock + private QuestionItemDomainService questionItemDomainService; + + @Mock + private ItemDomainService itemDomainService; + + @Mock + private ItemImgDomainService itemImgDomainService; + + @Mock + private ItemScrapDomainService itemScrapDomainService; + + @Test + @DisplayName("질문 아이템 목록이 null이면 기존 아이템만 삭제한다.") + void saveItemsWithNullRequestTest() { + // given + QuestionHowabout question = createQuestion(1L); + + // when + questionItemManager.saveItems(null, question); + + // then + verify(questionItemDomainService).deleteAllByQuestionId(question.getId()); + verify(itemDomainService, never()).findById(org.mockito.ArgumentMatchers.anyLong()); + verify(questionItemDomainService, never()).saveAll(org.mockito.ArgumentMatchers.anyList()); + } + + @Test + @DisplayName("질문 아이템 목록을 저장한다.") + void saveItemsTest() { + // given + QuestionHowabout question = createQuestion(1L); + Item firstItem = createItem(10L, "첫 번째 아이템"); + Item secondItem = createItem(20L, "두 번째 아이템"); + List itemRequests = List.of( + QuestionItemReqDto.builder() + .itemId(firstItem.getId()) + .description("대표 아이템") + .representFlag(true) + .sortOrder(1) + .build(), + QuestionItemReqDto.builder() + .itemId(secondItem.getId()) + .description("추가 아이템") + .representFlag(false) + .sortOrder(2) + .build() + ); + + when(itemDomainService.findById(firstItem.getId())).thenReturn(firstItem); + when(itemDomainService.findById(secondItem.getId())).thenReturn(secondItem); + + // when + questionItemManager.saveItems(itemRequests, question); + + // then + verify(questionItemDomainService).deleteAllByQuestionId(question.getId()); + verify(itemDomainService).findById(firstItem.getId()); + verify(itemDomainService).findById(secondItem.getId()); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(questionItemDomainService).saveAll(captor.capture()); + + List savedQuestionItems = captor.getValue(); + assertThat(savedQuestionItems).hasSize(2); + assertThat(savedQuestionItems.get(0).getQuestion()).isEqualTo(question); + assertThat(savedQuestionItems.get(0).getItem()).isEqualTo(firstItem); + assertThat(savedQuestionItems.get(0).getDescription()).isEqualTo("대표 아이템"); + assertThat(savedQuestionItems.get(0).getRepresentFlag()).isTrue(); + assertThat(savedQuestionItems.get(0).getSortOrder()).isEqualTo(1); + assertThat(savedQuestionItems.get(1).getQuestion()).isEqualTo(question); + assertThat(savedQuestionItems.get(1).getItem()).isEqualTo(secondItem); + assertThat(savedQuestionItems.get(1).getDescription()).isEqualTo("추가 아이템"); + assertThat(savedQuestionItems.get(1).getRepresentFlag()).isFalse(); + assertThat(savedQuestionItems.get(1).getSortOrder()).isEqualTo(2); + } + + @Test + @DisplayName("질문 아이템 응답에 아이템 정보와 투표 데이터를 포함한다.") + void getQuestionItemResponsesTest() { + // given + Long questionId = 1L; + QuestionHowabout question = createQuestion(questionId); + Item item = createItem(10L, "아이템"); + QuestionItem questionItem = QuestionItem.builder() + .question(question) + .item(item) + .description("질문 아이템") + .representFlag(true) + .sortOrder(2) + .build(); + ItemImg mainImage = ItemImg.builder() + .item(item) + .itemImgUrl("https://item-image.test/main.jpg") + .representFlag(true) + .sortOrder(1) + .build(); + QuestionVoteDataDto voteData = QuestionVoteDataDto.builder() + .voteNum(5L) + .votePercent(50.0) + .build(); + + when(questionItemDomainService.findAllByQuestionId(questionId)).thenReturn(List.of(questionItem)); + when(itemImgDomainService.findMainImg(item.getId())).thenReturn(mainImage); + when(itemScrapDomainService.getItemScrapStatus(item, List.of())).thenReturn(false); + + // when + List responses = questionItemManager.getQuestionItemResponses( + questionId, + List.of(), + sortOrder -> voteData + ); + + // then + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getItem().getItemId()).isEqualTo(item.getId()); + assertThat(responses.get(0).getItem().getImgUrl()).isEqualTo("https://item-image.test/main.jpg"); + assertThat(responses.get(0).getDescription()).isEqualTo("질문 아이템"); + assertThat(responses.get(0).getRepresentFlag()).isTrue(); + assertThat(responses.get(0).getSortOrder()).isEqualTo(2); + assertThat(responses.get(0).getVoteNum()).isEqualTo(5L); + assertThat(responses.get(0).getVotePercent()).isEqualTo(50.0); + } + + private QuestionHowabout createQuestion(Long id) { + return QuestionHowabout.builder() + .id(id) + .title("질문 제목") + .content("질문 내용") + .build(); + } + + private Item createItem(Long id, String name) { + return Item.builder() + .id(id) + .newBrand(NewBrand.builder() + .id(30L) + .brandName("브랜드") + .build()) + .newCeleb(NewCeleb.builder() + .id(40L) + .celebName("셀럽") + .build()) + .name(name) + .price(0) + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionResponseAssemblerTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionResponseAssemblerTest.java new file mode 100644 index 00000000..d0a5ba35 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/helper/QuestionResponseAssemblerTest.java @@ -0,0 +1,386 @@ +package com.sluv.api.domain.question.helper; + +import com.sluv.api.question.dto.QuestionHomeResDto; +import com.sluv.api.question.helper.QuestionImageManager; +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.domain.auth.enums.SnsType; +import com.sluv.domain.celeb.entity.Celeb; +import com.sluv.domain.comment.repository.CommentRepository; +import com.sluv.domain.item.entity.Item; +import com.sluv.domain.question.dto.QuestionImgSimpleDto; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionFind; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.entity.QuestionRecommendCategory; +import com.sluv.domain.question.repository.QuestionLikeRepository; +import com.sluv.domain.question.repository.QuestionRecommendCategoryRepository; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.repository.UserRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionResponseAssemblerTest { + + @InjectMocks + private QuestionResponseAssembler questionResponseAssembler; + + @Mock + private QuestionImageManager questionImageManager; + + @Mock + private QuestionLikeRepository questionLikeRepository; + + @Mock + private QuestionRecommendCategoryRepository questionRecommendCategoryRepository; + + @Mock + private CommentRepository commentRepository; + + @Mock + private UserRepository userRepository; + + @Test + @DisplayName("Buy 질문 응답에 질문 이미지와 아이템 대표 이미지를 포함한다.") + void getQuestionSimpleResponseWithBuyTest() { + // given + User writer = createUser(10L); + QuestionBuy question = QuestionBuy.builder() + .id(1L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + Item item = createItem(20L); + + when(questionImageManager.getQuestionImages(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://question-image.test/1.jpg", 1L))); + when(questionImageManager.getBuyItemMainImages(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://item-image.test/1.jpg", 1L))); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(2L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(3L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponse(question); + + // then + assertThat(response.getQType()).isEqualTo("Buy"); + assertThat(response.getId()).isEqualTo(question.getId()); + assertThat(response.getUser().getId()).isEqualTo(writer.getId()); + assertThat(response.getLikeNum()).isEqualTo(2L); + assertThat(response.getCommentNum()).isEqualTo(3L); + assertThat(response.getImgList()).hasSize(1); + assertThat(response.getImgList().get(0).getImgUrl()).isEqualTo("https://question-image.test/1.jpg"); + assertThat(response.getItemImgList()).hasSize(1); + assertThat(response.getItemImgList().get(0).getImgUrl()).isEqualTo("https://item-image.test/1.jpg"); + } + + @Test + @DisplayName("Find 질문 응답에 셀럽 이름을 포함하고 이미지/카테고리는 조회하지 않는다.") + void getQuestionSimpleResponseWithFindTest() { + // given + User writer = createUser(10L); + Celeb parentCeleb = createCeleb(1L, null, "부모 셀럽"); + Celeb childCeleb = createCeleb(2L, parentCeleb, "자식 셀럽"); + QuestionFind question = QuestionFind.builder() + .id(2L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .celeb(childCeleb) + .build(); + + when(questionImageManager.getQuestionImages(question)).thenReturn(null); + when(questionImageManager.getBuyItemMainImages(question)).thenReturn(null); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(1L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(4L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponse(question); + + // then + assertThat(response.getQType()).isEqualTo("Find"); + assertThat(response.getCelebName()).isEqualTo("부모 셀럽 자식 셀럽"); + assertThat(response.getImgList()).isNull(); + assertThat(response.getItemImgList()).isNull(); + assertThat(response.getCategoryName()).isNull(); + verify(questionRecommendCategoryRepository, never()).findAllByQuestionId(question.getId()); + } + + @Test + @DisplayName("대표 이미지가 필요한 Find 질문 응답에는 질문 대표 이미지와 아이템 대표 이미지를 포함한다.") + void getQuestionSimpleResponseWithMainImageAndFindTest() { + // given + User writer = createUser(10L); + Celeb celeb = createCeleb(2L, null, "셀럽"); + QuestionFind question = QuestionFind.builder() + .id(5L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .celeb(celeb) + .build(); + Item item = createItem(20L); + + when(questionImageManager.getQuestionImagesWithMainImage(question)) + .thenReturn(List.of( + new QuestionImgSimpleDto("https://question-image.test/main.jpg", 1L), + new QuestionImgSimpleDto("https://item-image.test/main.jpg", 1L) + )); + when(questionImageManager.getItemMainImagesForCard(question)).thenReturn(List.of()); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(1L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(2L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question); + + // then + assertThat(response.getQType()).isEqualTo("Find"); + assertThat(response.getImgList()).hasSize(2); + assertThat(response.getImgList().get(0).getImgUrl()).isEqualTo("https://question-image.test/main.jpg"); + assertThat(response.getImgList().get(1).getImgUrl()).isEqualTo("https://item-image.test/main.jpg"); + assertThat(response.getItemImgList()).isEmpty(); + assertThat(response.getCategoryName()).isNull(); + } + + @Test + @DisplayName("How 질문 응답은 공통 응답 값만 포함한다.") + void getQuestionSimpleResponseWithHowTest() { + // given + User writer = createUser(10L); + QuestionHowabout question = QuestionHowabout.builder() + .id(3L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + + when(questionImageManager.getQuestionImages(question)).thenReturn(null); + when(questionImageManager.getBuyItemMainImages(question)).thenReturn(null); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(5L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(6L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponse(question); + + // then + assertThat(response.getQType()).isEqualTo("How"); + assertThat(response.getLikeNum()).isEqualTo(5L); + assertThat(response.getCommentNum()).isEqualTo(6L); + assertThat(response.getImgList()).isNull(); + assertThat(response.getItemImgList()).isNull(); + assertThat(response.getCategoryName()).isNull(); + verify(questionRecommendCategoryRepository, never()).findAllByQuestionId(question.getId()); + } + + @Test + @DisplayName("Recommend 질문 응답에 카테고리 이름 목록을 포함한다.") + void getQuestionSimpleResponseWithRecommendTest() { + // given + User writer = createUser(10L); + QuestionRecommend question = QuestionRecommend.builder() + .id(4L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + List categories = List.of( + QuestionRecommendCategory.toEntity(question, "상의"), + QuestionRecommendCategory.toEntity(question, "아우터") + ); + + when(questionImageManager.getQuestionImages(question)).thenReturn(null); + when(questionImageManager.getBuyItemMainImages(question)).thenReturn(null); + when(questionRecommendCategoryRepository.findAllByQuestionId(question.getId())).thenReturn(categories); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(7L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(8L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponse(question); + + // then + assertThat(response.getQType()).isEqualTo("Recommend"); + assertThat(response.getCategoryName()).containsExactly("상의", "아우터"); + assertThat(response.getImgList()).isNull(); + assertThat(response.getItemImgList()).isNull(); + } + + @Test + @DisplayName("대표 이미지가 필요한 Buy 질문 응답에는 모든 질문 이미지와 모든 아이템 대표 이미지를 분리해서 포함한다.") + void getQuestionSimpleResponseWithMainImageAndBuyTest() { + // given + User writer = createUser(10L); + QuestionBuy question = QuestionBuy.builder() + .id(6L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + Item item = createItem(20L); + + when(questionImageManager.getQuestionImagesWithMainImage(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://question-image.test/1.jpg", 1L))); + when(questionImageManager.getItemMainImagesForCard(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://item-image.test/1.jpg", 1L))); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(3L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(4L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question); + + // then + assertThat(response.getQType()).isEqualTo("Buy"); + assertThat(response.getImgList()).hasSize(1); + assertThat(response.getImgList().get(0).getImgUrl()).isEqualTo("https://question-image.test/1.jpg"); + assertThat(response.getItemImgList()).hasSize(1); + assertThat(response.getItemImgList().get(0).getImgUrl()).isEqualTo("https://item-image.test/1.jpg"); + } + + @Test + @DisplayName("대표 이미지가 필요한 Recommend 질문 응답에는 카테고리 이름을 포함한다.") + void getQuestionSimpleResponseWithMainImageAndRecommendTest() { + // given + User writer = createUser(10L); + QuestionRecommend question = QuestionRecommend.builder() + .id(7L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + List categories = List.of( + QuestionRecommendCategory.toEntity(question, "상의"), + QuestionRecommendCategory.toEntity(question, "신발") + ); + + when(questionImageManager.getQuestionImagesWithMainImage(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://question-image.test/recommend.jpg", 1L))); + when(questionImageManager.getItemMainImagesForCard(question)).thenReturn(List.of()); + when(questionRecommendCategoryRepository.findAllByQuestionId(question.getId())).thenReturn(categories); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(5L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(6L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question); + + // then + assertThat(response.getQType()).isEqualTo("Recommend"); + assertThat(response.getImgList()).hasSize(1); + assertThat(response.getImgList().get(0).getImgUrl()).isEqualTo("https://question-image.test/recommend.jpg"); + assertThat(response.getCategoryName()).containsExactly("상의", "신발"); + assertThat(response.getItemImgList()).isEmpty(); + } + + @Test + @DisplayName("홈 질문 응답은 Buy 질문의 질문 이미지와 아이템 대표 이미지를 합쳐 정렬한다.") + void getQuestionHomeResponseWithBuyTest() { + // given + User writer = createUser(10L); + QuestionBuy question = QuestionBuy.builder() + .id(9L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + Item item = createItem(20L); + + when(questionImageManager.getQuestionImagesForHome(question)) + .thenReturn(List.of( + new QuestionImgSimpleDto("https://item-image.test/1.jpg", 1L), + new QuestionImgSimpleDto("https://question-image.test/2.jpg", 2L) + )); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionHomeResDto response = questionResponseAssembler.getQuestionHomeResponse(question); + + // then + assertThat(response.getQType()).isEqualTo("Buy"); + assertThat(response.getImgList()).extracting("imgUrl") + .containsExactly("https://item-image.test/1.jpg", "https://question-image.test/2.jpg"); + } + + @Test + @DisplayName("모든 이미지가 필요한 질문 응답에는 질문 이미지와 아이템 대표 이미지를 분리해서 포함한다.") + void getQuestionSimpleResponseWithImagesTest() { + // given + User writer = createUser(10L); + QuestionRecommend question = QuestionRecommend.builder() + .id(10L) + .user(writer) + .title("질문 제목") + .content("질문 내용") + .build(); + Item item = createItem(20L); + List categories = List.of( + QuestionRecommendCategory.toEntity(question, "상의"), + QuestionRecommendCategory.toEntity(question, "신발") + ); + + when(questionImageManager.getAllQuestionImages(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://question-image.test/all.jpg", 1L))); + when(questionImageManager.getAllItemMainImages(question)) + .thenReturn(List.of(new QuestionImgSimpleDto("https://item-image.test/all.jpg", 2L))); + when(questionRecommendCategoryRepository.findAllByQuestionId(question.getId())).thenReturn(categories); + when(questionLikeRepository.countByQuestionId(question.getId())).thenReturn(3L); + when(commentRepository.countByQuestionId(question.getId())).thenReturn(4L); + when(userRepository.findById(writer.getId())).thenReturn(Optional.of(writer)); + + // when + QuestionSimpleResDto response = questionResponseAssembler.getQuestionSimpleResponseWithImages(question); + + // then + assertThat(response.getQType()).isEqualTo("Recommend"); + assertThat(response.getImgList()).hasSize(1); + assertThat(response.getItemImgList()).hasSize(1); + assertThat(response.getCategoryName()).containsExactly("상의", "신발"); + assertThat(response.getLikeNum()).isEqualTo(3L); + assertThat(response.getCommentNum()).isEqualTo(4L); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("test@sluv.com") + .nickname("작성자") + .snsType(SnsType.ETC) + .profileImgUrl("https://profile.test/profile.jpg") + .build(); + } + + private Item createItem(Long id) { + return Item.builder() + .id(id) + .name("아이템") + .price(10000) + .build(); + } + + private Celeb createCeleb(Long id, Celeb parent, String name) { + return Celeb.builder() + .id(id) + .parent(parent) + .celebNameKr(name) + .celebNameEn(name) + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionFeedServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionFeedServiceTest.java new file mode 100644 index 00000000..e3b52658 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionFeedServiceTest.java @@ -0,0 +1,331 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.common.response.PaginationResponse; +import com.sluv.api.question.dto.QuestionBuySimpleResDto; +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.api.question.service.QuestionFeedService; +import com.sluv.api.question.service.QuestionVoteService; +import com.sluv.domain.auth.enums.SnsType; +import com.sluv.domain.brand.entity.NewBrand; +import com.sluv.domain.celeb.entity.NewCeleb; +import com.sluv.domain.item.entity.Item; +import com.sluv.domain.item.entity.ItemImg; +import com.sluv.domain.item.service.ItemImgDomainService; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.Question; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionFind; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionImg; +import com.sluv.domain.question.entity.QuestionItem; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.entity.QuestionVote; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionImgDomainService; +import com.sluv.domain.question.service.QuestionItemDomainService; +import com.sluv.domain.question.service.QuestionVoteDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.entity.UserBlock; +import com.sluv.domain.user.service.UserBlockDomainService; +import com.sluv.domain.user.service.UserDomainService; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionFeedServiceTest { + + @InjectMocks + private QuestionFeedService questionFeedService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private UserBlockDomainService userBlockDomainService; + + @Mock + private QuestionResponseAssembler questionResponseAssembler; + + @Mock + private QuestionImgDomainService questionImgDomainService; + + @Mock + private QuestionItemDomainService questionItemDomainService; + + @Mock + private ItemImgDomainService itemImgDomainService; + + @Mock + private QuestionVoteDomainService questionVoteDomainService; + + @Mock + private UserDomainService userDomainService; + + @Mock + private QuestionVoteService questionVoteService; + + @Test + @DisplayName("전체 질문 피드를 조회한다.") + void getTotalQuestionsTest() { + // given + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 10); + User user = createUser(userId); + User blockedUser = createUser(2L); + UserBlock userBlock = createUserBlock(user, blockedUser); + Question question = QuestionHowabout.builder().id(10L).user(user).title("전체 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + Page questions = new PageImpl<>(List.of(question), pageable, 1); + + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of(userBlock)); + when(questionDomainService.getTotalQuestionList(List.of(blockedUser.getId()), pageable)).thenReturn(questions); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + PaginationResponse result = questionFeedService.getTotalQuestions(userId, pageable); + + // then + assertThat(result.getContent()).containsExactly(response); + verify(questionDomainService).getTotalQuestionList(List.of(blockedUser.getId()), pageable); + } + + @Test + @DisplayName("비회원 전체 질문 피드는 차단 유저를 조회하지 않는다.") + void getTotalQuestionsWithoutUserTest() { + // given + Pageable pageable = PageRequest.of(0, 10); + User writer = createUser(1L); + Question question = QuestionHowabout.builder().id(11L).user(writer).title("전체 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + Page questions = new PageImpl<>(List.of(question), pageable, 1); + + when(questionDomainService.getTotalQuestionList(List.of(), pageable)).thenReturn(questions); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + PaginationResponse result = questionFeedService.getTotalQuestions(null, pageable); + + // then + assertThat(result.getContent()).containsExactly(response); + verify(userBlockDomainService, never()).getAllBlockedUser(org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + @DisplayName("Find 질문 피드를 조회한다.") + void getFindQuestionsTest() { + // given + Long userId = 1L; + Long celebId = 20L; + Boolean isNewCeleb = false; + Pageable pageable = PageRequest.of(0, 10); + User user = createUser(userId); + QuestionFind question = QuestionFind.builder().id(12L).user(user).title("Find 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + Page questions = new PageImpl<>(List.of(question), pageable, 1); + + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of()); + when(questionDomainService.getQuestionFindList(celebId, isNewCeleb, List.of(), pageable)) + .thenReturn(questions); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + PaginationResponse result = questionFeedService.getFindQuestions( + userId, + celebId, + isNewCeleb, + pageable + ); + + // then + assertThat(result.getContent()).containsExactly(response); + verify(questionDomainService).getQuestionFindList(celebId, isNewCeleb, List.of(), pageable); + } + + @Test + @DisplayName("How 질문 피드를 조회한다.") + void getHowaboutQuestionsTest() { + // given + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 10); + User user = createUser(userId); + QuestionHowabout question = QuestionHowabout.builder().id(13L).user(user).title("How 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + Page questions = new PageImpl<>(List.of(question), pageable, 1); + + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of()); + when(questionDomainService.getQuestionHowaboutList(List.of(), pageable)) + .thenReturn(questions); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + PaginationResponse result = questionFeedService.getHowaboutQuestions(userId, pageable); + + // then + assertThat(result.getContent()).containsExactly(response); + verify(questionDomainService).getQuestionHowaboutList(List.of(), pageable); + } + + @Test + @DisplayName("Recommend 질문 피드를 조회한다.") + void getRecommendQuestionsTest() { + // given + Long userId = 1L; + String hashtag = "상의"; + Pageable pageable = PageRequest.of(0, 10); + User user = createUser(userId); + QuestionRecommend question = QuestionRecommend.builder().id(14L).user(user).title("Recommend 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + Page questions = new PageImpl<>(List.of(question), pageable, 1); + + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of()); + when(questionDomainService.getQuestionRecommendList(hashtag, List.of(), pageable)) + .thenReturn(questions); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + PaginationResponse result = questionFeedService.getRecommendQuestions( + userId, + hashtag, + pageable + ); + + // then + assertThat(result.getContent()).containsExactly(response); + verify(questionDomainService).getQuestionRecommendList(hashtag, List.of(), pageable); + } + + @Test + @DisplayName("Buy 질문 피드를 조회한다.") + void getBuyQuestionsTest() { + // given + Long userId = 1L; + String voteStatus = "all"; + Pageable pageable = PageRequest.of(0, 10); + User user = createUser(userId); + User writer = createUser(2L); + User blockedUser = createUser(3L); + UserBlock userBlock = createUserBlock(user, blockedUser); + LocalDateTime voteEndTime = LocalDateTime.of(2026, 4, 20, 12, 0); + QuestionBuy question = QuestionBuy.builder() + .id(15L) + .user(writer) + .title("Buy 질문") + .content("질문 내용") + .voteEndTime(voteEndTime) + .build(); + QuestionImg questionImg = QuestionImg.builder() + .question(question) + .imgUrl("https://question-image.test/buy.jpg") + .sortOrder(1) + .representFlag(true) + .build(); + Item item = createItem(20L); + QuestionItem questionItem = QuestionItem.builder() + .question(question) + .item(item) + .sortOrder(2) + .representFlag(true) + .build(); + ItemImg itemImg = ItemImg.builder() + .item(item) + .itemImgUrl("https://item-image.test/buy.jpg") + .sortOrder(1) + .representFlag(true) + .build(); + QuestionVote questionVote = QuestionVote.builder() + .question(question) + .user(user) + .voteSortOrder(2L) + .build(); + Page questions = new PageImpl<>(List.of(question), pageable, 1); + + when(userDomainService.findById(userId)).thenReturn(user); + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of(userBlock)); + when(questionDomainService.getQuestionBuyList(voteStatus, List.of(blockedUser.getId()), pageable)) + .thenReturn(questions); + when(questionImgDomainService.findAllByQuestionId(question.getId())).thenReturn(List.of(questionImg)); + when(questionItemDomainService.findAllByQuestionId(question.getId())).thenReturn(List.of(questionItem)); + when(itemImgDomainService.findMainImg(item.getId())).thenReturn(itemImg); + when(questionVoteService.getVoteData(question.getId(), 1L)).thenReturn(QuestionVoteDataDto.of(4L, 40.0)); + when(questionVoteService.getVoteData(question.getId(), 2L)).thenReturn(QuestionVoteDataDto.of(6L, 60.0)); + when(questionVoteService.getTotalVoteCount( + org.mockito.ArgumentMatchers.anyList(), + org.mockito.ArgumentMatchers.anyList() + )).thenReturn(10L); + when(questionVoteDomainService.findByQuestionIdAndUserIdOrNull(question.getId(), user.getId())) + .thenReturn(questionVote); + when(userDomainService.findByIdOrNull(writer.getId())).thenReturn(writer); + + // when + PaginationResponse result = questionFeedService.getBuyQuestions( + userId, + voteStatus, + pageable + ); + + // then + assertThat(result.getContent()).hasSize(1); + QuestionBuySimpleResDto response = result.getContent().get(0); + assertThat(response.getQType()).isEqualTo("Buy"); + assertThat(response.getVoteNum()).isEqualTo(10L); + assertThat(response.getVoteStatus()).isTrue(); + assertThat(response.getSelectedVoteNum()).isEqualTo(2L); + assertThat(response.getImgList()).hasSize(1); + assertThat(response.getItemImgList()).hasSize(1); + verify(questionDomainService).getQuestionBuyList(voteStatus, List.of(blockedUser.getId()), pageable); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user@test.com") + .snsType(SnsType.ETC) + .build(); + } + + private UserBlock createUserBlock(User user, User blockedUser) { + return UserBlock.builder() + .user(user) + .blockedUser(blockedUser) + .build(); + } + + private QuestionSimpleResDto createResponse(Long questionId) { + return QuestionSimpleResDto.builder() + .id(questionId) + .title("질문 피드") + .build(); + } + + private Item createItem(Long id) { + return Item.builder() + .id(id) + .newBrand(NewBrand.builder() + .id(30L) + .brandName("브랜드") + .build()) + .newCeleb(NewCeleb.builder() + .id(40L) + .celebName("셀럽") + .build()) + .name("아이템") + .price(10000) + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionLikeServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionLikeServiceTest.java new file mode 100644 index 00000000..ccc390df --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionLikeServiceTest.java @@ -0,0 +1,105 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.question.service.QuestionLikeService; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionLikeDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import com.sluv.infra.alarm.service.QuestionAlarmService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionLikeServiceTest { + + @InjectMocks + private QuestionLikeService questionLikeService; + + @Mock + private UserDomainService userDomainService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private QuestionLikeDomainService questionLikeDomainService; + + @Mock + private QuestionAlarmService questionAlarmService; + + @Test + @DisplayName("질문 좋아요 내역이 있으면 좋아요를 삭제한다.") + void deleteQuestionLikeTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionHowabout question = createQuestion(questionId); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionLikeDomainService.existsByQuestionIdAndUserId(questionId, userId)).thenReturn(true); + when(questionDomainService.findById(questionId)).thenReturn(question); + + // when + questionLikeService.postQuestionLike(userId, questionId); + + // then + verify(questionLikeDomainService).deleteByQuestionIdAndUserId(questionId, userId); + verify(questionLikeDomainService, never()).saveQuestionLike( + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any() + ); + verify(questionAlarmService, never()).sendAlarmAboutQuestionLike( + org.mockito.ArgumentMatchers.anyLong(), + org.mockito.ArgumentMatchers.anyLong() + ); + } + + @Test + @DisplayName("질문 좋아요 내역이 없으면 좋아요를 저장하고 알림을 보낸다.") + void saveQuestionLikeTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionHowabout question = createQuestion(questionId); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionLikeDomainService.existsByQuestionIdAndUserId(questionId, userId)).thenReturn(false); + when(questionDomainService.findById(questionId)).thenReturn(question); + + // when + questionLikeService.postQuestionLike(userId, questionId); + + // then + verify(questionLikeDomainService).saveQuestionLike(user, question); + verify(questionAlarmService).sendAlarmAboutQuestionLike(userId, questionId); + verify(questionLikeDomainService, never()).deleteByQuestionIdAndUserId( + org.mockito.ArgumentMatchers.anyLong(), + org.mockito.ArgumentMatchers.anyLong() + ); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user@test.com") + .build(); + } + + private QuestionHowabout createQuestion(Long id) { + return QuestionHowabout.builder() + .id(id) + .title("질문 제목") + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionRankServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionRankServiceTest.java new file mode 100644 index 00000000..698162d5 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionRankServiceTest.java @@ -0,0 +1,123 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.common.response.PaginationResponse; +import com.sluv.api.question.dto.QuestionHomeResDto; +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.api.question.service.QuestionRankService; +import com.sluv.domain.auth.enums.SnsType; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.entity.UserBlock; +import com.sluv.domain.user.service.UserBlockDomainService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionRankServiceTest { + + @InjectMocks + private QuestionRankService questionRankService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private UserBlockDomainService userBlockDomainService; + + @Mock + private QuestionResponseAssembler questionResponseAssembler; + + @Test + @DisplayName("일간 인기 질문을 조회한다.") + void getDailyHotQuestionsTest() { + // given + Long userId = 1L; + User user = createUser(userId); + User blockedUser = createUser(2L); + User writer = createUser(3L); + QuestionBuy question = QuestionBuy.builder() + .id(10L) + .user(writer) + .title("일간 인기 질문") + .build(); + QuestionHomeResDto response = QuestionHomeResDto.builder() + .qType("Buy") + .id(question.getId()) + .title(question.getTitle()) + .build(); + + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of(createUserBlock(user, blockedUser))); + when(questionDomainService.getDailyHotQuestion(List.of(blockedUser.getId()))).thenReturn(List.of(question)); + when(questionResponseAssembler.getQuestionHomeResponse(question)).thenReturn(response); + + // when + List result = questionRankService.getDailyHotQuestions(userId); + + // then + assertThat(result).containsExactly(response); + } + + @Test + @DisplayName("주간 인기 질문을 조회한다.") + void getWeeklyHotQuestionsTest() { + // given + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 10); + User user = createUser(userId); + User blockedUser = createUser(2L); + User writer = createUser(3L); + QuestionRecommend question = QuestionRecommend.builder() + .id(11L) + .user(writer) + .title("주간 인기 질문") + .content("질문 내용") + .build(); + QuestionSimpleResDto response = QuestionSimpleResDto.builder() + .qType("Recommend") + .id(question.getId()) + .title(question.getTitle()) + .build(); + + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of(createUserBlock(user, blockedUser))); + when(questionDomainService.getWeeklyHotQuestion(List.of(blockedUser.getId()), pageable)) + .thenReturn(new PageImpl<>(List.of(question), pageable, 1)); + when(questionResponseAssembler.getQuestionSimpleResponseWithImages(question)).thenReturn(response); + + // when + PaginationResponse result = questionRankService.getWeeklyHotQuestions(userId, pageable); + + // then + assertThat(result.getContent()).containsExactly(response); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user@test.com") + .nickname("사용자") + .snsType(SnsType.ETC) + .build(); + } + + private UserBlock createUserBlock(User user, User blockedUser) { + return UserBlock.builder() + .user(user) + .blockedUser(blockedUser) + .build(); + } + +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionReportServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionReportServiceTest.java new file mode 100644 index 00000000..34bc6ca4 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionReportServiceTest.java @@ -0,0 +1,110 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.question.dto.QuestionReportReqDto; +import com.sluv.api.question.service.QuestionReportService; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.enums.QuestionReportReason; +import com.sluv.domain.question.exception.QuestionReportDuplicateException; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionReportDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionReportServiceTest { + + @InjectMocks + private QuestionReportService questionReportService; + + @Mock + private UserDomainService userDomainService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private QuestionReportDomainService questionReportDomainService; + + @Test + @DisplayName("질문 신고 내역이 없으면 신고를 저장한다.") + void postQuestionReportTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionHowabout question = createQuestion(questionId); + QuestionReportReqDto request = createReportRequest(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionReportDomainService.existsByQuestionIdAndReporterId(questionId, userId)).thenReturn(false); + when(questionDomainService.findById(questionId)).thenReturn(question); + + // when + questionReportService.postQuestionReport(userId, questionId, request); + + // then + verify(questionReportDomainService).saveQuestionReport( + user, + question, + request.getReason(), + request.getContent() + ); + } + + @Test + @DisplayName("질문 신고 내역이 있으면 중복 신고 예외가 발생한다.") + void postQuestionReportDuplicateTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionReportReqDto request = createReportRequest(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionReportDomainService.existsByQuestionIdAndReporterId(questionId, userId)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> questionReportService.postQuestionReport(userId, questionId, request)) + .isInstanceOf(QuestionReportDuplicateException.class); + + verify(questionDomainService, never()).findById(org.mockito.ArgumentMatchers.anyLong()); + verify(questionReportDomainService, never()).saveQuestionReport( + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.anyString() + ); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user@test.com") + .build(); + } + + private QuestionHowabout createQuestion(Long id) { + return QuestionHowabout.builder() + .id(id) + .title("질문 제목") + .build(); + } + + private QuestionReportReqDto createReportRequest() { + return QuestionReportReqDto.builder() + .reason(QuestionReportReason.SPAM_OR_AD) + .content("신고 내용") + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionServiceTest.java new file mode 100644 index 00000000..9d81ab1e --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionServiceTest.java @@ -0,0 +1,333 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.moderation.service.QuestionModerationService; +import com.sluv.api.moderation.service.QuestionModerationStatusService; +import com.sluv.api.question.dto.QuestionBuyPostReqDto; +import com.sluv.api.question.dto.QuestionFindPostReqDto; +import com.sluv.api.question.dto.QuestionHowaboutPostReqDto; +import com.sluv.api.question.dto.QuestionPostResDto; +import com.sluv.api.question.dto.QuestionRecommendPostReqDto; +import com.sluv.api.question.helper.QuestionImageManager; +import com.sluv.api.question.helper.QuestionItemManager; +import com.sluv.api.question.service.QuestionService; +import com.sluv.api.question.service.QuestionVoteService; +import com.sluv.domain.celeb.service.CelebDomainService; +import com.sluv.domain.celeb.service.NewCelebDomainService; +import com.sluv.domain.closet.service.ClosetDomainService; +import com.sluv.domain.comment.service.CommentDomainService; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionFind; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.enums.QuestionStatus; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionLikeDomainService; +import com.sluv.domain.question.service.QuestionRecommendCategoryDomainService; +import com.sluv.domain.question.service.QuestionVoteDomainService; +import com.sluv.domain.question.service.RecentQuestionDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionServiceTest { + + @InjectMocks + private QuestionService questionService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private QuestionRecommendCategoryDomainService questionRecommendCategoryDomainService; + + @Mock + private QuestionLikeDomainService questionLikeDomainService; + + @Mock + private CommentDomainService commentDomainService; + + @Mock + private CelebDomainService celebDomainService; + + @Mock + private NewCelebDomainService newCelebDomainService; + + @Mock + private RecentQuestionDomainService recentQuestionDomainService; + + @Mock + private ClosetDomainService closetDomainService; + + @Mock + private QuestionVoteDomainService questionVoteDomainService; + + @Mock + private UserDomainService userDomainService; + + @Mock + private QuestionImageManager questionImageManager; + + @Mock + private QuestionItemManager questionItemManager; + + @Mock + private QuestionVoteService questionVoteService; + + @Mock + private QuestionModerationService questionModerationService; + + @Mock + private QuestionModerationStatusService questionModerationStatusService; + + @Test + @DisplayName("찾아주세요 질문 신규 등록 후 검수 작업 생성을 요청한다.") + void postQuestionFindCreatesModerationJobTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionFindPostReqDto request = QuestionFindPostReqDto.builder() + .title("찾아주세요") + .content("질문 내용") + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionFind savedQuestion = QuestionFind.builder() + .id(1L) + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getInitialQuestionStatus()).thenReturn(QuestionStatus.ACTIVE); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionFind.class))) + .thenReturn(savedQuestion); + + // when + QuestionPostResDto response = questionService.postQuestionFind(userId, request); + + // then + assertThat(response.getId()).isEqualTo(savedQuestion.getId()); + verify(questionModerationService).createQuestionJobIfEnabled(savedQuestion); + } + + @Test + @DisplayName("찾아주세요 질문 수정 후 검수 작업 생성을 요청한다.") + void updateQuestionFindCreatesModerationJobTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionFindPostReqDto request = QuestionFindPostReqDto.builder() + .id(1L) + .title("수정된 찾아주세요") + .content("수정된 질문 내용") + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionFind savedQuestion = QuestionFind.builder() + .id(request.getId()) + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getUpdateQuestionStatus()).thenReturn(QuestionStatus.ACTIVE); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionFind.class))) + .thenReturn(savedQuestion); + + // when + QuestionPostResDto response = questionService.postQuestionFind(userId, request); + + // then + assertThat(response.getId()).isEqualTo(savedQuestion.getId()); + verify(questionModerationService).createQuestionJobIfEnabled(savedQuestion); + } + + @Test + @DisplayName("이 중에 뭐 살까 질문 신규 등록 후 검수 작업 생성을 요청한다.") + void postQuestionBuyCreatesModerationJobTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionBuyPostReqDto request = QuestionBuyPostReqDto.builder() + .title("뭐 살까") + .voteEndTime(LocalDateTime.now().plusDays(1)) + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionBuy savedQuestion = QuestionBuy.builder() + .id(2L) + .user(user) + .title(request.getTitle()) + .voteEndTime(request.getVoteEndTime()) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getInitialQuestionStatus()).thenReturn(QuestionStatus.ACTIVE); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionBuy.class))) + .thenReturn(savedQuestion); + + // when + QuestionPostResDto response = questionService.postQuestionBuy(userId, request); + + // then + assertThat(response.getId()).isEqualTo(savedQuestion.getId()); + verify(questionModerationService).createQuestionJobIfEnabled(savedQuestion); + } + + @Test + @DisplayName("이거 어때 질문 신규 등록 후 검수 작업 생성을 요청한다.") + void postQuestionHowaboutCreatesModerationJobTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionHowaboutPostReqDto request = QuestionHowaboutPostReqDto.builder() + .title("이거 어때") + .content("질문 내용") + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionHowabout savedQuestion = QuestionHowabout.builder() + .id(3L) + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getInitialQuestionStatus()).thenReturn(QuestionStatus.ACTIVE); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionHowabout.class))) + .thenReturn(savedQuestion); + + // when + QuestionPostResDto response = questionService.postQuestionHowabout(userId, request); + + // then + assertThat(response.getId()).isEqualTo(savedQuestion.getId()); + verify(questionModerationService).createQuestionJobIfEnabled(savedQuestion); + } + + @Test + @DisplayName("추천해줘 질문 신규 등록 후 검수 작업 생성을 요청한다.") + void postQuestionRecommendCreatesModerationJobTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionRecommendPostReqDto request = QuestionRecommendPostReqDto.builder() + .title("추천해줘") + .content("질문 내용") + .categoryNameList(List.of("상의")) + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionRecommend savedQuestion = QuestionRecommend.builder() + .id(4L) + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getInitialQuestionStatus()).thenReturn(QuestionStatus.ACTIVE); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionRecommend.class))) + .thenReturn(savedQuestion); + + // when + QuestionPostResDto response = questionService.postQuestionRecommend(userId, request); + + // then + assertThat(response.getId()).isEqualTo(savedQuestion.getId()); + verify(questionModerationService).createQuestionJobIfEnabled(savedQuestion); + } + + @Test + @DisplayName("찾아주세요 질문 신규 등록 시 생성 PENDING 플래그가 반영된 상태로 저장한다.") + void postQuestionFindAppliesInitialModerationStatusTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionFindPostReqDto request = QuestionFindPostReqDto.builder() + .title("찾아주세요") + .content("질문 내용") + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionFind savedQuestion = QuestionFind.builder() + .id(1L) + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .questionStatus(QuestionStatus.PENDING) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getInitialQuestionStatus()).thenReturn(QuestionStatus.PENDING); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionFind.class))) + .thenReturn(savedQuestion); + + // when + questionService.postQuestionFind(userId, request); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(QuestionFind.class); + verify(questionDomainService).saveQuestion(captor.capture()); + assertThat(captor.getValue().getQuestionStatus()).isEqualTo(QuestionStatus.PENDING); + } + + @Test + @DisplayName("찾아주세요 질문 수정 시 수정 PENDING 플래그가 반영된 상태로 저장한다.") + void updateQuestionFindAppliesUpdateModerationStatusTest() { + // given + Long userId = 10L; + User user = createUser(userId); + QuestionFindPostReqDto request = QuestionFindPostReqDto.builder() + .id(1L) + .title("수정된 찾아주세요") + .content("수정된 질문 내용") + .imgList(List.of()) + .itemList(List.of()) + .build(); + QuestionFind savedQuestion = QuestionFind.builder() + .id(request.getId()) + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .questionStatus(QuestionStatus.PENDING) + .build(); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionModerationStatusService.getUpdateQuestionStatus()).thenReturn(QuestionStatus.PENDING); + when(questionDomainService.saveQuestion(org.mockito.ArgumentMatchers.any(QuestionFind.class))) + .thenReturn(savedQuestion); + + // when + questionService.postQuestionFind(userId, request); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(QuestionFind.class); + verify(questionDomainService).saveQuestion(captor.capture()); + assertThat(captor.getValue().getQuestionStatus()).isEqualTo(QuestionStatus.PENDING); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user" + id + "@example.com") + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionVoteServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionVoteServiceTest.java new file mode 100644 index 00000000..d2a17a22 --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionVoteServiceTest.java @@ -0,0 +1,177 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.question.dto.QuestionVoteDataDto; +import com.sluv.api.question.dto.QuestionImgResDto; +import com.sluv.api.question.dto.QuestionItemResDto; +import com.sluv.api.question.dto.QuestionVoteReqDto; +import com.sluv.api.question.service.QuestionVoteService; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionVote; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.question.service.QuestionVoteDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.service.UserDomainService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionVoteServiceTest { + + @InjectMocks + private QuestionVoteService questionVoteService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private QuestionVoteDomainService questionVoteDomainService; + + @Mock + private UserDomainService userDomainService; + + @Test + @DisplayName("투표 내역이 없으면 질문에 투표한다.") + void postQuestionVoteTest() { + // given + Long userId = 1L; + Long questionId = 10L; + Long voteSortOrder = 2L; + User user = createUser(userId); + QuestionBuy question = createQuestion(questionId); + QuestionVoteReqDto request = createVoteRequest(voteSortOrder); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionVoteDomainService.findByQuestionIdAndUserIdOrNull(questionId, userId)).thenReturn(null); + when(questionDomainService.findById(questionId)).thenReturn(question); + + // when + questionVoteService.postQuestionVote(userId, questionId, request); + + // then + verify(questionVoteDomainService).saveQuestionVote(question, user, voteSortOrder); + verify(questionVoteDomainService, never()).deleteById(org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + @DisplayName("투표 내역이 있으면 기존 투표를 취소한다.") + void cancelQuestionVoteTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionVote questionVote = createQuestionVote(100L, 2L); + QuestionVoteReqDto request = createVoteRequest(2L); + + when(userDomainService.findById(userId)).thenReturn(user); + when(questionVoteDomainService.findByQuestionIdAndUserIdOrNull(questionId, userId)).thenReturn(questionVote); + + // when + questionVoteService.postQuestionVote(userId, questionId, request); + + // then + verify(questionVoteDomainService).deleteById(questionVote.getId()); + verify(questionDomainService, never()).findById(org.mockito.ArgumentMatchers.anyLong()); + verify(questionVoteDomainService, never()).saveQuestionVote( + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.anyLong() + ); + } + + @Test + @DisplayName("특정 투표 순서의 투표 수와 퍼센트를 계산한다.") + void getVoteDataTest() { + // given + Long questionId = 10L; + Long voteSortOrder = 2L; + List questionVotes = List.of( + createQuestionVote(1L, 1L), + createQuestionVote(2L, 2L), + createQuestionVote(3L, 2L) + ); + + when(questionVoteDomainService.findAllByQuestionId(questionId)).thenReturn(questionVotes); + + // when + QuestionVoteDataDto voteData = questionVoteService.getVoteData(questionId, voteSortOrder); + + // then + assertThat(voteData.getVoteNum()).isEqualTo(2L); + assertThat(voteData.getVotePercent()).isEqualTo(66.7); + } + + @Test + @DisplayName("투표가 없으면 투표 수와 퍼센트를 0으로 반환한다.") + void getVoteDataWithEmptyVoteTest() { + // given + Long questionId = 10L; + Long voteSortOrder = 2L; + + when(questionVoteDomainService.findAllByQuestionId(questionId)).thenReturn(List.of()); + + // when + QuestionVoteDataDto voteData = questionVoteService.getVoteData(questionId, voteSortOrder); + + // then + assertThat(voteData.getVoteNum()).isEqualTo(0L); + assertThat(voteData.getVotePercent()).isEqualTo(0.0); + } + + @Test + @DisplayName("질문 이미지와 아이템의 총 투표 수를 계산한다.") + void getTotalVoteCountTest() { + // given + List questionImages = List.of( + QuestionImgResDto.builder().voteNum(1L).build(), + QuestionImgResDto.builder().voteNum(2L).build() + ); + List questionItems = List.of( + QuestionItemResDto.builder().voteNum(3L).build(), + QuestionItemResDto.builder().voteNum(4L).build() + ); + + // when + Long totalVoteCount = questionVoteService.getTotalVoteCount(questionImages, questionItems); + + // then + assertThat(totalVoteCount).isEqualTo(10L); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user@test.com") + .build(); + } + + private QuestionBuy createQuestion(Long id) { + return QuestionBuy.builder() + .id(id) + .title("질문 제목") + .build(); + } + + private QuestionVoteReqDto createVoteRequest(Long voteSortOrder) { + QuestionVoteReqDto request = new QuestionVoteReqDto(); + request.setVoteSortOrder(voteSortOrder); + return request; + } + + private QuestionVote createQuestionVote(Long id, Long voteSortOrder) { + return QuestionVote.builder() + .id(id) + .voteSortOrder(voteSortOrder) + .build(); + } +} diff --git a/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionWaitServiceTest.java b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionWaitServiceTest.java new file mode 100644 index 00000000..b8db590d --- /dev/null +++ b/sluv-api/src/test/java/com/sluv/api/domain/question/service/QuestionWaitServiceTest.java @@ -0,0 +1,183 @@ +package com.sluv.api.domain.question.service; + +import com.sluv.api.question.helper.QuestionResponseAssembler; +import com.sluv.api.question.service.QuestionWaitService; +import com.sluv.domain.celeb.entity.Celeb; +import com.sluv.domain.celeb.service.CelebDomainService; +import com.sluv.domain.question.dto.QuestionSimpleResDto; +import com.sluv.domain.question.entity.QuestionBuy; +import com.sluv.domain.question.entity.QuestionFind; +import com.sluv.domain.question.entity.QuestionHowabout; +import com.sluv.domain.question.entity.QuestionRecommend; +import com.sluv.domain.question.service.QuestionDomainService; +import com.sluv.domain.user.entity.User; +import com.sluv.domain.user.entity.UserBlock; +import com.sluv.domain.user.service.UserBlockDomainService; +import com.sluv.domain.user.service.UserDomainService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuestionWaitServiceTest { + + @InjectMocks + private QuestionWaitService questionWaitService; + + @Mock + private QuestionDomainService questionDomainService; + + @Mock + private UserDomainService userDomainService; + + @Mock + private UserBlockDomainService userBlockDomainService; + + @Mock + private CelebDomainService celebDomainService; + + @Mock + private QuestionResponseAssembler questionResponseAssembler; + + @Test + @DisplayName("Buy 대기 질문을 조회한다.") + void getWaitQuestionBuyTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + User blockedUser = createUser(2L); + UserBlock userBlock = createUserBlock(user, blockedUser); + Celeb interestedCeleb = createCeleb(3L); + QuestionBuy question = QuestionBuy.builder().id(100L).user(user).title("Buy 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + + when(userDomainService.findById(userId)).thenReturn(user); + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of(userBlock)); + when(celebDomainService.findInterestedCeleb(user)).thenReturn(List.of(interestedCeleb)); + when(questionDomainService.getWaitQuestionBuy(user, questionId, List.of(interestedCeleb), List.of(blockedUser.getId()))) + .thenReturn(List.of(question)); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + List result = questionWaitService.getWaitQuestionBuy(userId, questionId); + + // then + assertThat(result).containsExactly(response); + verify(questionDomainService).getWaitQuestionBuy(user, questionId, List.of(interestedCeleb), List.of(blockedUser.getId())); + } + + @Test + @DisplayName("Find 대기 질문을 조회한다.") + void getWaitQuestionFindTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + Celeb interestedCeleb = createCeleb(3L); + QuestionFind question = QuestionFind.builder().id(101L).user(user).title("Find 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + + when(userDomainService.findById(userId)).thenReturn(user); + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of()); + when(celebDomainService.findInterestedCeleb(user)).thenReturn(List.of(interestedCeleb)); + when(questionDomainService.getWaitQuestionFind(user, questionId, List.of(interestedCeleb), List.of())) + .thenReturn(List.of(question)); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + List result = questionWaitService.getWaitQuestionFind(userId, questionId); + + // then + assertThat(result).containsExactly(response); + verify(questionDomainService).getWaitQuestionFind(user, questionId, List.of(interestedCeleb), List.of()); + } + + @Test + @DisplayName("How 대기 질문을 조회한다.") + void getWaitQuestionHowaboutTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionHowabout question = QuestionHowabout.builder().id(102L).user(user).title("How 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + + when(userDomainService.findById(userId)).thenReturn(user); + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of()); + when(questionDomainService.getWaitQuestionHowabout(user, questionId, List.of())) + .thenReturn(List.of(question)); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + List result = questionWaitService.getWaitQuestionHowabout(userId, questionId); + + // then + assertThat(result).containsExactly(response); + verify(questionDomainService).getWaitQuestionHowabout(user, questionId, List.of()); + verify(celebDomainService, never()).findInterestedCeleb(user); + } + + @Test + @DisplayName("Recommend 대기 질문을 조회한다.") + void getWaitQuestionRecommendTest() { + // given + Long userId = 1L; + Long questionId = 10L; + User user = createUser(userId); + QuestionRecommend question = QuestionRecommend.builder().id(103L).user(user).title("Recommend 질문").build(); + QuestionSimpleResDto response = createResponse(question.getId()); + + when(userDomainService.findById(userId)).thenReturn(user); + when(userBlockDomainService.getAllBlockedUser(userId)).thenReturn(List.of()); + when(questionDomainService.getWaitQuestionRecommend(user, questionId, List.of())) + .thenReturn(List.of(question)); + when(questionResponseAssembler.getQuestionSimpleResponseWithMainImage(question)).thenReturn(response); + + // when + List result = questionWaitService.getWaitQuestionRecommend(userId, questionId); + + // then + assertThat(result).containsExactly(response); + verify(questionDomainService).getWaitQuestionRecommend(user, questionId, List.of()); + verify(celebDomainService, never()).findInterestedCeleb(user); + } + + private User createUser(Long id) { + return User.builder() + .id(id) + .email("user@test.com") + .build(); + } + + private UserBlock createUserBlock(User user, User blockedUser) { + return UserBlock.builder() + .user(user) + .blockedUser(blockedUser) + .build(); + } + + private Celeb createCeleb(Long id) { + return Celeb.builder() + .id(id) + .celebNameKr("셀럽") + .celebNameEn("celeb") + .build(); + } + + private QuestionSimpleResDto createResponse(Long questionId) { + return QuestionSimpleResDto.builder() + .id(questionId) + .title("대기 질문") + .build(); + } +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/celeb/entity/InterestedCeleb.java b/sluv-domain/src/main/java/com/sluv/domain/celeb/entity/InterestedCeleb.java index 0072a2c5..2808e659 100644 --- a/sluv-domain/src/main/java/com/sluv/domain/celeb/entity/InterestedCeleb.java +++ b/sluv-domain/src/main/java/com/sluv/domain/celeb/entity/InterestedCeleb.java @@ -8,7 +8,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.lang.Nullable; +import jakarta.annotation.Nullable; @Entity @Getter diff --git a/sluv-domain/src/main/java/com/sluv/domain/featureflag/entity/FeatureFlag.java b/sluv-domain/src/main/java/com/sluv/domain/featureflag/entity/FeatureFlag.java new file mode 100644 index 00000000..77b25c7a --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/featureflag/entity/FeatureFlag.java @@ -0,0 +1,44 @@ +package com.sluv.domain.featureflag.entity; + +import com.sluv.domain.common.entity.BaseEntity; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "feature_flag") +public class FeatureFlag extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feature_flag_id") + private Long id; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(length = 100, nullable = false, unique = true) + private FeatureFlagKey flagKey; + + @NotNull + @Column(nullable = false) + private boolean enabled; + + @Size(max = 255) + private String description; +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/featureflag/enums/FeatureFlagKey.java b/sluv-domain/src/main/java/com/sluv/domain/featureflag/enums/FeatureFlagKey.java new file mode 100644 index 00000000..cd339eba --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/featureflag/enums/FeatureFlagKey.java @@ -0,0 +1,8 @@ +package com.sluv.domain.featureflag.enums; + +public enum FeatureFlagKey { + MODERATION_JOB_CREATION, + MODERATION_QUESTION_CREATE_PENDING, + MODERATION_QUESTION_UPDATE_PENDING, + MODERATION_AUTO_APPLY_RESULT +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/featureflag/repository/FeatureFlagRepository.java b/sluv-domain/src/main/java/com/sluv/domain/featureflag/repository/FeatureFlagRepository.java new file mode 100644 index 00000000..159c66b7 --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/featureflag/repository/FeatureFlagRepository.java @@ -0,0 +1,11 @@ +package com.sluv.domain.featureflag.repository; + +import com.sluv.domain.featureflag.entity.FeatureFlag; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FeatureFlagRepository extends JpaRepository { + Optional findByFlagKey(FeatureFlagKey flagKey); +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/featureflag/service/FeatureFlagDomainService.java b/sluv-domain/src/main/java/com/sluv/domain/featureflag/service/FeatureFlagDomainService.java new file mode 100644 index 00000000..cbc9f33c --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/featureflag/service/FeatureFlagDomainService.java @@ -0,0 +1,29 @@ +package com.sluv.domain.featureflag.service; + +import com.sluv.domain.featureflag.entity.FeatureFlag; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import com.sluv.domain.featureflag.repository.FeatureFlagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FeatureFlagDomainService { + + private final FeatureFlagRepository featureFlagRepository; + + public boolean isEnabled(FeatureFlagKey flagKey) { + return featureFlagRepository.findByFlagKey(flagKey) + .map(FeatureFlag::isEnabled) + .orElseGet(() -> getDefaultValue(flagKey)); + } + + private boolean getDefaultValue(FeatureFlagKey flagKey) { + return switch (flagKey) { + case MODERATION_JOB_CREATION -> true; + case MODERATION_QUESTION_CREATE_PENDING, + MODERATION_QUESTION_UPDATE_PENDING, + MODERATION_AUTO_APPLY_RESULT -> false; + }; + } +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/entity/ModerationJob.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/entity/ModerationJob.java new file mode 100644 index 00000000..827e21a3 --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/entity/ModerationJob.java @@ -0,0 +1,75 @@ +package com.sluv.domain.moderation.entity; + +import com.sluv.domain.common.entity.BaseEntity; +import com.sluv.domain.moderation.enums.ModerationJobStatus; +import com.sluv.domain.moderation.enums.ModerationTargetType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "moderation_job") +public class ModerationJob extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "moderation_job_id") + private Long id; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(length = 45, nullable = false) + private ModerationTargetType targetType; + + @NotNull + @Column(nullable = false) + private Long targetId; + + @NotNull + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(length = 45, nullable = false, columnDefinition = "varchar(45) default 'REQUESTED'") + private ModerationJobStatus status = ModerationJobStatus.REQUESTED; + + private Long requestedBy; + + private Double score; + + @Size(max = 255) + private String reason; + + @Column(columnDefinition = "TEXT") + private String resultPayload; + + private Long reviewedBy; + + private LocalDateTime reviewedAt; + + private LocalDateTime processedAt; + + public static ModerationJob createQuestionJob(Long questionId, Long requestedBy) { + return ModerationJob.builder() + .targetType(ModerationTargetType.QUESTION) + .targetId(questionId) + .requestedBy(requestedBy) + .status(ModerationJobStatus.REQUESTED) + .build(); + } +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/enums/ModerationJobStatus.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/enums/ModerationJobStatus.java new file mode 100644 index 00000000..2d669245 --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/enums/ModerationJobStatus.java @@ -0,0 +1,11 @@ +package com.sluv.domain.moderation.enums; + +public enum ModerationJobStatus { + REQUESTED, + PROCESSING, + NEEDS_REVIEW, + APPROVED, + REJECTED, + FAILED, + CANCELED +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/enums/ModerationTargetType.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/enums/ModerationTargetType.java new file mode 100644 index 00000000..08ae78ca --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/enums/ModerationTargetType.java @@ -0,0 +1,5 @@ +package com.sluv.domain.moderation.enums; + +public enum ModerationTargetType { + QUESTION +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/ModerationJobRepository.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/ModerationJobRepository.java new file mode 100644 index 00000000..f9429147 --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/ModerationJobRepository.java @@ -0,0 +1,8 @@ +package com.sluv.domain.moderation.repository; + +import com.sluv.domain.moderation.entity.ModerationJob; +import com.sluv.domain.moderation.repository.impl.ModerationJobRepositoryCustom; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ModerationJobRepository extends JpaRepository, ModerationJobRepositoryCustom { +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/impl/ModerationJobRepositoryCustom.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/impl/ModerationJobRepositoryCustom.java new file mode 100644 index 00000000..d44840a7 --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/impl/ModerationJobRepositoryCustom.java @@ -0,0 +1,7 @@ +package com.sluv.domain.moderation.repository.impl; + +import com.sluv.domain.moderation.enums.ModerationTargetType; + +public interface ModerationJobRepositoryCustom { + boolean existsOpenJob(ModerationTargetType targetType, Long targetId); +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/impl/ModerationJobRepositoryImpl.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/impl/ModerationJobRepositoryImpl.java new file mode 100644 index 00000000..cad6873e --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/repository/impl/ModerationJobRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.sluv.domain.moderation.repository.impl; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.sluv.domain.moderation.enums.ModerationJobStatus; +import com.sluv.domain.moderation.enums.ModerationTargetType; +import java.util.List; +import lombok.RequiredArgsConstructor; + +import static com.sluv.domain.moderation.entity.QModerationJob.moderationJob; + +@RequiredArgsConstructor +public class ModerationJobRepositoryImpl implements ModerationJobRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public boolean existsOpenJob(ModerationTargetType targetType, Long targetId) { + Integer result = jpaQueryFactory.selectOne() + .from(moderationJob) + .where(moderationJob.targetType.eq(targetType) + .and(moderationJob.targetId.eq(targetId)) + .and(moderationJob.status.in(getOpenStatuses())) + ) + .fetchFirst(); + + return result != null; + } + + private List getOpenStatuses() { + return List.of( + ModerationJobStatus.REQUESTED, + ModerationJobStatus.PROCESSING, + ModerationJobStatus.NEEDS_REVIEW + ); + } +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/moderation/service/ModerationJobDomainService.java b/sluv-domain/src/main/java/com/sluv/domain/moderation/service/ModerationJobDomainService.java new file mode 100644 index 00000000..30eab2dd --- /dev/null +++ b/sluv-domain/src/main/java/com/sluv/domain/moderation/service/ModerationJobDomainService.java @@ -0,0 +1,22 @@ +package com.sluv.domain.moderation.service; + +import com.sluv.domain.moderation.entity.ModerationJob; +import com.sluv.domain.moderation.enums.ModerationTargetType; +import com.sluv.domain.moderation.repository.ModerationJobRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ModerationJobDomainService { + + private final ModerationJobRepository moderationJobRepository; + + public void createQuestionJobIfAbsent(Long questionId, Long requestedBy) { + if (moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, questionId)) { + return; + } + + moderationJobRepository.save(ModerationJob.createQuestionJob(questionId, requestedBy)); + } +} diff --git a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionBuy.java b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionBuy.java index 2f920b4a..4eb1e4bf 100644 --- a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionBuy.java +++ b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionBuy.java @@ -27,6 +27,11 @@ public QuestionBuy(Long id, User user, String title, String content, Long search } public static QuestionBuy toEntity(User user, Long questionId, String title, LocalDateTime voteEndTime) { + return toEntity(user, questionId, title, voteEndTime, QuestionStatus.ACTIVE); + } + + public static QuestionBuy toEntity(User user, Long questionId, String title, LocalDateTime voteEndTime, + QuestionStatus questionStatus) { QuestionBuyBuilder builder = QuestionBuy.builder(); if (questionId != null) { @@ -37,7 +42,7 @@ public static QuestionBuy toEntity(User user, Long questionId, String title, Loc .user(user) .title(title) .voteEndTime(voteEndTime) - .questionStatus(QuestionStatus.ACTIVE) + .questionStatus(questionStatus) .build(); } } diff --git a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionFind.java b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionFind.java index c6345701..0c3d815c 100644 --- a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionFind.java +++ b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionFind.java @@ -38,6 +38,11 @@ public QuestionFind(Long id, User user, String title, String content, Long searc public static QuestionFind toEntity(User user, Long questionId, String title, String content, Celeb celeb, NewCeleb newCeleb) { + return toEntity(user, questionId, title, content, celeb, newCeleb, QuestionStatus.ACTIVE); + } + + public static QuestionFind toEntity(User user, Long questionId, String title, String content, Celeb celeb, + NewCeleb newCeleb, QuestionStatus questionStatus) { QuestionFindBuilder builder = QuestionFind.builder(); if (questionId != null) { @@ -51,7 +56,7 @@ public static QuestionFind toEntity(User user, Long questionId, String title, St .content(content) .celeb(celeb) .newCeleb(newCeleb) - .questionStatus(QuestionStatus.ACTIVE) + .questionStatus(questionStatus) .build(); } } diff --git a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionHowabout.java b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionHowabout.java index 3cc00f93..bfd97299 100644 --- a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionHowabout.java +++ b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionHowabout.java @@ -20,6 +20,11 @@ public QuestionHowabout(Long id, User user, String title, String content, Long s } public static QuestionHowabout toEntity(User user, Long questionId, String title, String content) { + return toEntity(user, questionId, title, content, QuestionStatus.ACTIVE); + } + + public static QuestionHowabout toEntity(User user, Long questionId, String title, String content, + QuestionStatus questionStatus) { QuestionHowaboutBuilder builder = QuestionHowabout.builder(); if (questionId != null) { @@ -30,7 +35,7 @@ public static QuestionHowabout toEntity(User user, Long questionId, String title .user(user) .title(title) .content(content) - .questionStatus(QuestionStatus.ACTIVE) + .questionStatus(questionStatus) .build(); } } diff --git a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionRecommend.java b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionRecommend.java index 9a91b08c..62af1ec9 100644 --- a/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionRecommend.java +++ b/sluv-domain/src/main/java/com/sluv/domain/question/entity/QuestionRecommend.java @@ -20,6 +20,11 @@ public QuestionRecommend(Long id, User user, String title, String content, Long } public static QuestionRecommend toEntity(User user, Long questionId, String title, String content) { + return toEntity(user, questionId, title, content, QuestionStatus.ACTIVE); + } + + public static QuestionRecommend toEntity(User user, Long questionId, String title, String content, + QuestionStatus questionStatus) { QuestionRecommendBuilder builder = QuestionRecommend.builder(); if (questionId != null) { @@ -31,7 +36,7 @@ public static QuestionRecommend toEntity(User user, Long questionId, String titl .user(user) .title(title) .content(content) - .questionStatus(QuestionStatus.ACTIVE) + .questionStatus(questionStatus) .build(); } } diff --git a/sluv-domain/src/main/java/com/sluv/domain/question/enums/QuestionStatus.java b/sluv-domain/src/main/java/com/sluv/domain/question/enums/QuestionStatus.java index c1ebc828..96c61c1d 100644 --- a/sluv-domain/src/main/java/com/sluv/domain/question/enums/QuestionStatus.java +++ b/sluv-domain/src/main/java/com/sluv/domain/question/enums/QuestionStatus.java @@ -2,6 +2,7 @@ public enum QuestionStatus { ACTIVE, + PENDING, BLOCKED, DELETED } diff --git a/sluv-domain/src/test/java/com/sluv/domain/featureflag/repository/FeatureFlagRepositoryTest.java b/sluv-domain/src/test/java/com/sluv/domain/featureflag/repository/FeatureFlagRepositoryTest.java new file mode 100644 index 00000000..2927507d --- /dev/null +++ b/sluv-domain/src/test/java/com/sluv/domain/featureflag/repository/FeatureFlagRepositoryTest.java @@ -0,0 +1,74 @@ +package com.sluv.domain.featureflag.repository; + +import com.sluv.domain.config.TestConfig; +import com.sluv.domain.featureflag.entity.FeatureFlag; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ContextConfiguration(classes = TestConfig.class) +@ActiveProfiles("test") +public class FeatureFlagRepositoryTest { + + @Autowired + private FeatureFlagRepository featureFlagRepository; + + @AfterEach + void clean() { + featureFlagRepository.deleteAll(); + } + + @Test + @DisplayName("flagKey로 Feature Flag를 조회한다.") + void findByFlagKeyTest() { + // given + FeatureFlag featureFlag = FeatureFlag.builder() + .flagKey(FeatureFlagKey.MODERATION_JOB_CREATION) + .enabled(true) + .description("게시글 업로드 시 검수 작업 생성 여부") + .build(); + featureFlagRepository.save(featureFlag); + + // when + Optional result = featureFlagRepository.findByFlagKey( + FeatureFlagKey.MODERATION_JOB_CREATION + ); + + // then + assertThat(result).isPresent(); + assertThat(result.get().getFlagKey()).isEqualTo(FeatureFlagKey.MODERATION_JOB_CREATION); + assertThat(result.get().isEnabled()).isTrue(); + } + + @Test + @DisplayName("비활성화된 Feature Flag를 저장하고 조회한다.") + void findDisabledFeatureFlagTest() { + // given + FeatureFlag featureFlag = FeatureFlag.builder() + .flagKey(FeatureFlagKey.MODERATION_AUTO_APPLY_RESULT) + .enabled(false) + .description("검수 결과 자동 반영 여부") + .build(); + featureFlagRepository.save(featureFlag); + + // when + Optional result = featureFlagRepository.findByFlagKey( + FeatureFlagKey.MODERATION_AUTO_APPLY_RESULT + ); + + // then + assertThat(result).isPresent(); + assertThat(result.get().getFlagKey()).isEqualTo(FeatureFlagKey.MODERATION_AUTO_APPLY_RESULT); + assertThat(result.get().isEnabled()).isFalse(); + } +} diff --git a/sluv-domain/src/test/java/com/sluv/domain/featureflag/service/FeatureFlagDomainServiceTest.java b/sluv-domain/src/test/java/com/sluv/domain/featureflag/service/FeatureFlagDomainServiceTest.java new file mode 100644 index 00000000..72da603b --- /dev/null +++ b/sluv-domain/src/test/java/com/sluv/domain/featureflag/service/FeatureFlagDomainServiceTest.java @@ -0,0 +1,102 @@ +package com.sluv.domain.featureflag.service; + +import com.sluv.domain.featureflag.entity.FeatureFlag; +import com.sluv.domain.featureflag.enums.FeatureFlagKey; +import com.sluv.domain.featureflag.repository.FeatureFlagRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FeatureFlagDomainServiceTest { + + @InjectMocks + private FeatureFlagDomainService featureFlagDomainService; + + @Mock + private FeatureFlagRepository featureFlagRepository; + + @Test + @DisplayName("Feature Flag가 DB에 있으면 DB 값을 반환한다.") + void isEnabledReturnsDatabaseValueTest() { + // given + FeatureFlag featureFlag = FeatureFlag.builder() + .flagKey(FeatureFlagKey.MODERATION_JOB_CREATION) + .enabled(false) + .description("게시글 업로드 시 검수 작업 생성 여부") + .build(); + + when(featureFlagRepository.findByFlagKey(FeatureFlagKey.MODERATION_JOB_CREATION)) + .thenReturn(Optional.of(featureFlag)); + + // when + boolean enabled = featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION); + + // then + assertThat(enabled).isFalse(); + } + + @Test + @DisplayName("MODERATION_JOB_CREATION이 DB에 없으면 기본값 true를 반환한다.") + void moderationJobCreationDefaultValueTest() { + // given + when(featureFlagRepository.findByFlagKey(FeatureFlagKey.MODERATION_JOB_CREATION)) + .thenReturn(Optional.empty()); + + // when + boolean enabled = featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_JOB_CREATION); + + // then + assertThat(enabled).isTrue(); + } + + @Test + @DisplayName("MODERATION_QUESTION_CREATE_PENDING이 DB에 없으면 기본값 false를 반환한다.") + void moderationQuestionCreatePendingDefaultValueTest() { + // given + when(featureFlagRepository.findByFlagKey(FeatureFlagKey.MODERATION_QUESTION_CREATE_PENDING)) + .thenReturn(Optional.empty()); + + // when + boolean enabled = featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_CREATE_PENDING); + + // then + assertThat(enabled).isFalse(); + } + + @Test + @DisplayName("MODERATION_AUTO_APPLY_RESULT가 DB에 없으면 기본값 false를 반환한다.") + void moderationAutoApplyResultDefaultValueTest() { + // given + when(featureFlagRepository.findByFlagKey(FeatureFlagKey.MODERATION_AUTO_APPLY_RESULT)) + .thenReturn(Optional.empty()); + + // when + boolean enabled = featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_AUTO_APPLY_RESULT); + + // then + assertThat(enabled).isFalse(); + } + + @Test + @DisplayName("MODERATION_QUESTION_UPDATE_PENDING이 DB에 없으면 기본값 false를 반환한다.") + void moderationQuestionUpdatePendingDefaultValueTest() { + // given + when(featureFlagRepository.findByFlagKey(FeatureFlagKey.MODERATION_QUESTION_UPDATE_PENDING)) + .thenReturn(Optional.empty()); + + // when + boolean enabled = featureFlagDomainService.isEnabled(FeatureFlagKey.MODERATION_QUESTION_UPDATE_PENDING); + + // then + assertThat(enabled).isFalse(); + } +} diff --git a/sluv-domain/src/test/java/com/sluv/domain/moderation/repository/ModerationJobRepositoryTest.java b/sluv-domain/src/test/java/com/sluv/domain/moderation/repository/ModerationJobRepositoryTest.java new file mode 100644 index 00000000..c3ffbc8e --- /dev/null +++ b/sluv-domain/src/test/java/com/sluv/domain/moderation/repository/ModerationJobRepositoryTest.java @@ -0,0 +1,124 @@ +package com.sluv.domain.moderation.repository; + +import com.sluv.domain.config.TestConfig; +import com.sluv.domain.moderation.entity.ModerationJob; +import com.sluv.domain.moderation.enums.ModerationJobStatus; +import com.sluv.domain.moderation.enums.ModerationTargetType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ContextConfiguration(classes = TestConfig.class) +@ActiveProfiles("test") +public class ModerationJobRepositoryTest { + + @Autowired + private ModerationJobRepository moderationJobRepository; + + @AfterEach + void clean() { + moderationJobRepository.deleteAll(); + } + + @Test + @DisplayName("moderation job을 저장한다.") + void saveModerationJobTest() { + // given + ModerationJob moderationJob = createJob(1L, ModerationJobStatus.REQUESTED); + + // when + ModerationJob savedJob = moderationJobRepository.save(moderationJob); + + // then + assertThat(savedJob.getId()).isNotNull(); + assertThat(savedJob.getTargetType()).isEqualTo(ModerationTargetType.QUESTION); + assertThat(savedJob.getTargetId()).isEqualTo(1L); + assertThat(savedJob.getStatus()).isEqualTo(ModerationJobStatus.REQUESTED); + assertThat(savedJob.getRequestedBy()).isEqualTo(10L); + } + + @Test + @DisplayName("REQUESTED 상태의 moderation job은 open job으로 판단한다.") + void requestedJobIsOpenJobTest() { + // given + moderationJobRepository.save(createJob(1L, ModerationJobStatus.REQUESTED)); + + // when + boolean exists = moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, 1L); + + // then + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("PROCESSING 상태의 moderation job은 open job으로 판단한다.") + void processingJobIsOpenJobTest() { + // given + moderationJobRepository.save(createJob(1L, ModerationJobStatus.PROCESSING)); + + // when + boolean exists = moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, 1L); + + // then + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("NEEDS_REVIEW 상태의 moderation job은 open job으로 판단한다.") + void needsReviewJobIsOpenJobTest() { + // given + moderationJobRepository.save(createJob(1L, ModerationJobStatus.NEEDS_REVIEW)); + + // when + boolean exists = moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, 1L); + + // then + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("완료 상태의 moderation job은 open job으로 판단하지 않는다.") + void completedJobIsNotOpenJobTest() { + // given + moderationJobRepository.save(createJob(1L, ModerationJobStatus.APPROVED)); + moderationJobRepository.save(createJob(1L, ModerationJobStatus.REJECTED)); + moderationJobRepository.save(createJob(1L, ModerationJobStatus.FAILED)); + moderationJobRepository.save(createJob(1L, ModerationJobStatus.CANCELED)); + + // when + boolean exists = moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, 1L); + + // then + assertThat(exists).isFalse(); + } + + @Test + @DisplayName("다른 targetId의 moderation job은 open job으로 판단하지 않는다.") + void differentTargetIdIsNotOpenJobTest() { + // given + moderationJobRepository.save(createJob(1L, ModerationJobStatus.REQUESTED)); + + // when + boolean exists = moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, 2L); + + // then + assertThat(exists).isFalse(); + } + + private ModerationJob createJob(Long targetId, ModerationJobStatus status) { + return ModerationJob.builder() + .targetType(ModerationTargetType.QUESTION) + .targetId(targetId) + .status(status) + .requestedBy(10L) + .reason("테스트") + .build(); + } +} diff --git a/sluv-domain/src/test/java/com/sluv/domain/moderation/service/ModerationJobDomainServiceTest.java b/sluv-domain/src/test/java/com/sluv/domain/moderation/service/ModerationJobDomainServiceTest.java new file mode 100644 index 00000000..609b58d5 --- /dev/null +++ b/sluv-domain/src/test/java/com/sluv/domain/moderation/service/ModerationJobDomainServiceTest.java @@ -0,0 +1,72 @@ +package com.sluv.domain.moderation.service; + +import com.sluv.domain.moderation.entity.ModerationJob; +import com.sluv.domain.moderation.enums.ModerationJobStatus; +import com.sluv.domain.moderation.enums.ModerationTargetType; +import com.sluv.domain.moderation.repository.ModerationJobRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ModerationJobDomainServiceTest { + + @InjectMocks + private ModerationJobDomainService moderationJobDomainService; + + @Mock + private ModerationJobRepository moderationJobRepository; + + @Test + @DisplayName("open job이 없으면 REQUESTED 상태의 질문 검수 작업을 생성한다.") + void createQuestionJobIfOpenJobDoesNotExistTest() { + // given + Long questionId = 1L; + Long requestedBy = 10L; + + when(moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, questionId)) + .thenReturn(false); + + // when + moderationJobDomainService.createQuestionJobIfAbsent(questionId, requestedBy); + + // then + verify(moderationJobRepository).existsOpenJob(ModerationTargetType.QUESTION, questionId); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ModerationJob.class); + verify(moderationJobRepository).save(captor.capture()); + + ModerationJob savedJob = captor.getValue(); + assertThat(savedJob.getTargetType()).isEqualTo(ModerationTargetType.QUESTION); + assertThat(savedJob.getTargetId()).isEqualTo(questionId); + assertThat(savedJob.getRequestedBy()).isEqualTo(requestedBy); + assertThat(savedJob.getStatus()).isEqualTo(ModerationJobStatus.REQUESTED); + } + + @Test + @DisplayName("open job이 있으면 질문 검수 작업을 중복 생성하지 않는다.") + void doesNotCreateQuestionJobIfOpenJobExistsTest() { + // given + Long questionId = 1L; + Long requestedBy = 10L; + + when(moderationJobRepository.existsOpenJob(ModerationTargetType.QUESTION, questionId)) + .thenReturn(true); + + // when + moderationJobDomainService.createQuestionJobIfAbsent(questionId, requestedBy); + + // then + verify(moderationJobRepository).existsOpenJob(ModerationTargetType.QUESTION, questionId); + verify(moderationJobRepository, never()).save(org.mockito.ArgumentMatchers.any()); + } +} diff --git a/sluv-domain/src/test/java/com/sluv/domain/question/entity/QuestionStatusTest.java b/sluv-domain/src/test/java/com/sluv/domain/question/entity/QuestionStatusTest.java new file mode 100644 index 00000000..d9cea00b --- /dev/null +++ b/sluv-domain/src/test/java/com/sluv/domain/question/entity/QuestionStatusTest.java @@ -0,0 +1,82 @@ +package com.sluv.domain.question.entity; + +import com.sluv.domain.question.enums.QuestionStatus; +import com.sluv.domain.user.entity.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QuestionStatusTest { + + @Test + @DisplayName("질문 상태에 PENDING이 존재한다.") + void questionStatusHasPendingTest() { + // when + QuestionStatus status = QuestionStatus.valueOf("PENDING"); + + // then + assertThat(status.name()).isEqualTo("PENDING"); + } + + @Test + @DisplayName("찾아주세요 질문 생성 시 상태는 ACTIVE다.") + void questionFindDefaultStatusIsActiveTest() { + // given + User user = createUser(); + + // when + QuestionFind question = QuestionFind.toEntity(user, null, "찾아주세요", "내용", null, null); + + // then + assertThat(question.getQuestionStatus()).isEqualTo(QuestionStatus.ACTIVE); + } + + @Test + @DisplayName("이 중에 뭐 살까 질문 생성 시 상태는 ACTIVE다.") + void questionBuyDefaultStatusIsActiveTest() { + // given + User user = createUser(); + + // when + QuestionBuy question = QuestionBuy.toEntity(user, null, "뭐 살까", LocalDateTime.now().plusDays(1)); + + // then + assertThat(question.getQuestionStatus()).isEqualTo(QuestionStatus.ACTIVE); + } + + @Test + @DisplayName("이거 어때 질문 생성 시 상태는 ACTIVE다.") + void questionHowaboutDefaultStatusIsActiveTest() { + // given + User user = createUser(); + + // when + QuestionHowabout question = QuestionHowabout.toEntity(user, null, "이거 어때", "내용"); + + // then + assertThat(question.getQuestionStatus()).isEqualTo(QuestionStatus.ACTIVE); + } + + @Test + @DisplayName("추천해줘 질문 생성 시 상태는 ACTIVE다.") + void questionRecommendDefaultStatusIsActiveTest() { + // given + User user = createUser(); + + // when + QuestionRecommend question = QuestionRecommend.toEntity(user, null, "추천해줘", "내용"); + + // then + assertThat(question.getQuestionStatus()).isEqualTo(QuestionStatus.ACTIVE); + } + + private User createUser() { + return User.builder() + .id(1L) + .email("user@example.com") + .build(); + } +}