diff --git a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt index acade395..3ef636c9 100644 --- a/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt +++ b/src/main/kotlin/picklab/backend/common/model/SuccessCode.kt @@ -62,6 +62,8 @@ enum class SuccessCode( CREATE_REVIEW_SUCCESS(HttpStatus.OK, "리뷰 생성에 성공했습니다."), UPDATE_REVIEW_SUCCESS(HttpStatus.OK, "리뷰 수정에 성공했습니다."), DELETE_REVIEW_SUCCESS(HttpStatus.OK, "리뷰 삭제에 성공했습니다."), + MARK_REVIEW_HELPFUL_SUCCESS(HttpStatus.OK, "리뷰에 도움돼요를 표시했습니다."), + UNMARK_REVIEW_HELPFUL_SUCCESS(HttpStatus.OK, "리뷰 도움돼요를 취소했습니다."), GET_JOB_RELEVANCE_AVG_SCORES_SUCCESS(HttpStatus.OK, "직무 관련도 평균 점수 조회에 성공했습니다."), GET_SATISFACTION_AVG_SCORES_SUCCESS(HttpStatus.OK, "활동별 만족도 통계 조회에 성공했습니다."), diff --git a/src/main/kotlin/picklab/backend/review/application/ReviewUseCase.kt b/src/main/kotlin/picklab/backend/review/application/ReviewUseCase.kt index 5b2ad963..7bce1fba 100644 --- a/src/main/kotlin/picklab/backend/review/application/ReviewUseCase.kt +++ b/src/main/kotlin/picklab/backend/review/application/ReviewUseCase.kt @@ -14,14 +14,15 @@ import picklab.backend.participation.domain.service.ActivityParticipationService import picklab.backend.review.application.mapper.toDetailView import picklab.backend.review.application.mapper.toEntity import picklab.backend.review.application.model.ActivityReviewListQueryRequest +import picklab.backend.review.application.model.ActivityReviewWithHelpful import picklab.backend.review.application.model.MyReviewListQueryRequest import picklab.backend.review.application.model.ReviewCreateCommand import picklab.backend.review.application.model.ReviewUpdateCommand -import picklab.backend.review.application.query.model.ActivityReviewListView import picklab.backend.review.application.query.model.MyReviewDetailView import picklab.backend.review.application.query.model.MyReviewListView import picklab.backend.review.application.service.ReviewOverviewQueryService import picklab.backend.review.domain.policy.ReviewApprovalDecider +import picklab.backend.review.domain.service.ReviewHelpfulService import picklab.backend.review.domain.service.ReviewService @Component @@ -32,6 +33,7 @@ class ReviewUseCase( private val activityParticipationService: ActivityParticipationService, private val reviewOverviewQueryService: ReviewOverviewQueryService, private val jobService: JobService, + private val reviewHelpfulService: ReviewHelpfulService, ) { fun createReview(command: ReviewCreateCommand) { val member = memberService.findActiveMember(command.memberId) @@ -75,13 +77,14 @@ class ReviewUseCase( return reviewOverviewQueryService.findMyReviews(member.id, pageable) } + @Transactional(readOnly = true) fun getReviewsByActivity( request: ActivityReviewListQueryRequest, activityId: Long, memberId: Long?, page: Int, size: Int, - ): Page { + ): Page { memberId?.let { memberService.findActiveMember(it) } val activity = activityService.mustFindById(activityId) val pageable = @@ -90,8 +93,40 @@ class ReviewUseCase( size, Sort.by("createdAt").descending(), ) - return reviewOverviewQueryService - .findActivityReviews(request, activity.id, pageable) + val reviews = + reviewOverviewQueryService + .findActivityReviews(request, activity.id, pageable) + val reviewIds = reviews.content.map { it.id } + val helpfulCounts = reviewHelpfulService.countByReviewIds(reviewIds) + val helpfulReviewIds = reviewHelpfulService.findHelpfulReviewIds(memberId, reviewIds) + + return reviews.map { review -> + ActivityReviewWithHelpful( + review = review, + helpfulCount = helpfulCounts[review.id] ?: 0L, + isHelpful = review.id in helpfulReviewIds, + ) + } + } + + @Transactional + fun markReviewHelpful( + memberId: Long, + reviewId: Long, + ) { + val member = memberService.findActiveMember(memberId) + val review = reviewService.mustFindById(reviewId) + reviewHelpfulService.markHelpful(member.id, review.id) + } + + @Transactional + fun unmarkReviewHelpful( + memberId: Long, + reviewId: Long, + ) { + val member = memberService.findActiveMember(memberId) + val review = reviewService.mustFindById(reviewId) + reviewHelpfulService.unmarkHelpful(member.id, review.id) } @Transactional diff --git a/src/main/kotlin/picklab/backend/review/application/model/ActivityReviewWithHelpful.kt b/src/main/kotlin/picklab/backend/review/application/model/ActivityReviewWithHelpful.kt new file mode 100644 index 00000000..1df13921 --- /dev/null +++ b/src/main/kotlin/picklab/backend/review/application/model/ActivityReviewWithHelpful.kt @@ -0,0 +1,9 @@ +package picklab.backend.review.application.model + +import picklab.backend.review.application.query.model.ActivityReviewListView + +data class ActivityReviewWithHelpful( + val review: ActivityReviewListView, + val helpfulCount: Long, + val isHelpful: Boolean, +) diff --git a/src/main/kotlin/picklab/backend/review/domain/entity/ReviewHelpful.kt b/src/main/kotlin/picklab/backend/review/domain/entity/ReviewHelpful.kt new file mode 100644 index 00000000..d5ec9f61 --- /dev/null +++ b/src/main/kotlin/picklab/backend/review/domain/entity/ReviewHelpful.kt @@ -0,0 +1,39 @@ +package picklab.backend.review.domain.entity + +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Index +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint +import org.hibernate.annotations.Comment +import picklab.backend.common.model.BaseEntity +import picklab.backend.member.domain.entity.Member + +@Entity +@Table( + name = "review_helpful", + uniqueConstraints = [ + UniqueConstraint( + name = "uk_review_helpful_member_review", + columnNames = ["member_id", "review_id"], + ), + ], + indexes = [ + Index( + name = "idx_review_helpful_review_id", + columnList = "review_id", + ), + ], +) +class ReviewHelpful( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + @Comment("회원 ID") + val member: Member, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id", nullable = false) + @Comment("리뷰 ID") + val review: Review, +) : BaseEntity() diff --git a/src/main/kotlin/picklab/backend/review/domain/repository/ReviewHelpfulCountProjection.kt b/src/main/kotlin/picklab/backend/review/domain/repository/ReviewHelpfulCountProjection.kt new file mode 100644 index 00000000..e288d4cb --- /dev/null +++ b/src/main/kotlin/picklab/backend/review/domain/repository/ReviewHelpfulCountProjection.kt @@ -0,0 +1,6 @@ +package picklab.backend.review.domain.repository + +interface ReviewHelpfulCountProjection { + val reviewId: Long + val helpfulCount: Long +} diff --git a/src/main/kotlin/picklab/backend/review/domain/repository/ReviewHelpfulRepository.kt b/src/main/kotlin/picklab/backend/review/domain/repository/ReviewHelpfulRepository.kt new file mode 100644 index 00000000..a110d0f5 --- /dev/null +++ b/src/main/kotlin/picklab/backend/review/domain/repository/ReviewHelpfulRepository.kt @@ -0,0 +1,61 @@ +package picklab.backend.review.domain.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import picklab.backend.review.domain.entity.ReviewHelpful + +interface ReviewHelpfulRepository : JpaRepository { + @Modifying + @Query( + nativeQuery = true, + value = """ + INSERT INTO review_helpful (member_id, review_id, created_at, updated_at) + VALUES (:memberId, :reviewId, NOW(), NOW()) + ON DUPLICATE KEY UPDATE member_id = member_id + """, + ) + fun upsert( + @Param("memberId") memberId: Long, + @Param("reviewId") reviewId: Long, + ): Int + + @Modifying + @Query( + """ + DELETE FROM ReviewHelpful helpful + WHERE helpful.member.id = :memberId + AND helpful.review.id = :reviewId + """, + ) + fun deleteByMemberIdAndReviewId( + @Param("memberId") memberId: Long, + @Param("reviewId") reviewId: Long, + ): Int + + @Query( + """ + SELECT helpful.review.id AS reviewId, COUNT(helpful.id) AS helpfulCount + FROM ReviewHelpful helpful + WHERE helpful.review.id IN :reviewIds + GROUP BY helpful.review.id + """, + ) + fun countByReviewIds( + @Param("reviewIds") reviewIds: Collection, + ): List + + @Query( + """ + SELECT helpful.review.id + FROM ReviewHelpful helpful + WHERE helpful.member.id = :memberId + AND helpful.review.id IN :reviewIds + """, + ) + fun findReviewIdsByMemberId( + @Param("memberId") memberId: Long, + @Param("reviewIds") reviewIds: Collection, + ): List +} diff --git a/src/main/kotlin/picklab/backend/review/domain/service/ReviewHelpfulService.kt b/src/main/kotlin/picklab/backend/review/domain/service/ReviewHelpfulService.kt new file mode 100644 index 00000000..9ef7c206 --- /dev/null +++ b/src/main/kotlin/picklab/backend/review/domain/service/ReviewHelpfulService.kt @@ -0,0 +1,46 @@ +package picklab.backend.review.domain.service + +import org.springframework.stereotype.Service +import picklab.backend.review.domain.repository.ReviewHelpfulRepository + +@Service +class ReviewHelpfulService( + private val reviewHelpfulRepository: ReviewHelpfulRepository, +) { + fun markHelpful( + memberId: Long, + reviewId: Long, + ) { + reviewHelpfulRepository.upsert(memberId, reviewId) + } + + fun unmarkHelpful( + memberId: Long, + reviewId: Long, + ) { + reviewHelpfulRepository.deleteByMemberIdAndReviewId(memberId, reviewId) + } + + fun countByReviewIds(reviewIds: Collection): Map { + if (reviewIds.isEmpty()) { + return emptyMap() + } + + return reviewHelpfulRepository + .countByReviewIds(reviewIds) + .associate { it.reviewId to it.helpfulCount } + } + + fun findHelpfulReviewIds( + memberId: Long?, + reviewIds: Collection, + ): Set { + if (memberId == null || reviewIds.isEmpty()) { + return emptySet() + } + + return reviewHelpfulRepository + .findReviewIdsByMemberId(memberId, reviewIds) + .toSet() + } +} diff --git a/src/main/kotlin/picklab/backend/review/entrypoint/ReviewApi.kt b/src/main/kotlin/picklab/backend/review/entrypoint/ReviewApi.kt index f549753b..8f8b0dc4 100644 --- a/src/main/kotlin/picklab/backend/review/entrypoint/ReviewApi.kt +++ b/src/main/kotlin/picklab/backend/review/entrypoint/ReviewApi.kt @@ -93,6 +93,24 @@ interface ReviewApi { @ParameterObject request: ActivityReviewListRequest, ): ResponseEntity>> + @Operation( + summary = "리뷰 도움돼요 표시", + description = "로그인한 사용자가 리뷰에 도움돼요를 표시합니다. 이미 표시한 경우에도 성공합니다.", + ) + fun markReviewHelpful( + @Parameter(description = "리뷰 ID값") @PathVariable id: Long, + member: MemberPrincipal, + ): ResponseEntity> + + @Operation( + summary = "리뷰 도움돼요 취소", + description = "로그인한 사용자가 리뷰의 도움돼요 표시를 취소합니다. 표시하지 않은 경우에도 성공합니다.", + ) + fun unmarkReviewHelpful( + @Parameter(description = "리뷰 ID값") @PathVariable id: Long, + member: MemberPrincipal, + ): ResponseEntity> + @Operation( summary = "리뷰 수정", description = "본인이 작성한 리뷰를 수정합니다.", diff --git a/src/main/kotlin/picklab/backend/review/entrypoint/ReviewController.kt b/src/main/kotlin/picklab/backend/review/entrypoint/ReviewController.kt index ecf15707..13ef2c6b 100644 --- a/src/main/kotlin/picklab/backend/review/entrypoint/ReviewController.kt +++ b/src/main/kotlin/picklab/backend/review/entrypoint/ReviewController.kt @@ -79,6 +79,24 @@ class ReviewController( .let { ResponseWrapper.success(SuccessCode.GET_REVIEWS, it) } .let { ResponseEntity.ok(it) } + @PostMapping("/v1/reviews/{id}/helpful") + override fun markReviewHelpful( + @PathVariable id: Long, + @AuthenticationPrincipal member: MemberPrincipal, + ): ResponseEntity> { + reviewUseCase.markReviewHelpful(member.memberId, id) + return ResponseEntity.ok(ResponseWrapper.success(SuccessCode.MARK_REVIEW_HELPFUL_SUCCESS)) + } + + @DeleteMapping("/v1/reviews/{id}/helpful") + override fun unmarkReviewHelpful( + @PathVariable id: Long, + @AuthenticationPrincipal member: MemberPrincipal, + ): ResponseEntity> { + reviewUseCase.unmarkReviewHelpful(member.memberId, id) + return ResponseEntity.ok(ResponseWrapper.success(SuccessCode.UNMARK_REVIEW_HELPFUL_SUCCESS)) + } + @PutMapping("/v1/reviews/{id}") override fun updateReview( @PathVariable id: Long, diff --git a/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewListMapper.kt b/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewListMapper.kt index fa294e94..d9118b46 100644 --- a/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewListMapper.kt +++ b/src/main/kotlin/picklab/backend/review/entrypoint/mapper/ReviewListMapper.kt @@ -1,7 +1,7 @@ package picklab.backend.review.entrypoint.mapper import picklab.backend.common.model.MemberPrincipal -import picklab.backend.review.application.query.model.ActivityReviewListView +import picklab.backend.review.application.model.ActivityReviewWithHelpful import picklab.backend.review.application.query.model.MyReviewListView import picklab.backend.review.entrypoint.response.ActivityReviewResponse import picklab.backend.review.entrypoint.response.MyReviewsResponse @@ -17,22 +17,25 @@ fun MyReviewListView.toResponse(): MyReviewsResponse = approvalStatus = approvalStatus, ) -fun ActivityReviewListView.toResponse(member: MemberPrincipal?): ActivityReviewResponse { +fun ActivityReviewWithHelpful.toResponse(member: MemberPrincipal?): ActivityReviewResponse { val isLoggedIn = member != null + val review = this.review return ActivityReviewResponse( - id = id, - overallScore = overallScore, - infoScore = infoScore, - difficultyScore = difficultyScore, - benefitScore = benefitScore, - jobGroup = jobGroup, - jobDetail = jobDetail, - participationDate = participationDate, - progressStatus = progressStatus, - summary = summary.takeIf { isLoggedIn }, - strength = strength.takeIf { isLoggedIn }, - weakness = weakness.takeIf { isLoggedIn }, - tips = tips.takeIf { isLoggedIn }, + id = review.id, + overallScore = review.overallScore, + infoScore = review.infoScore, + difficultyScore = review.difficultyScore, + benefitScore = review.benefitScore, + jobGroup = review.jobGroup, + jobDetail = review.jobDetail, + participationDate = review.participationDate, + progressStatus = review.progressStatus, + helpfulCount = helpfulCount, + isHelpful = isHelpful, + summary = review.summary.takeIf { isLoggedIn }, + strength = review.strength.takeIf { isLoggedIn }, + weakness = review.weakness.takeIf { isLoggedIn }, + tips = review.tips.takeIf { isLoggedIn }, ) } diff --git a/src/main/kotlin/picklab/backend/review/entrypoint/response/ActivityReviewResponse.kt b/src/main/kotlin/picklab/backend/review/entrypoint/response/ActivityReviewResponse.kt index da5aff75..3ace8323 100644 --- a/src/main/kotlin/picklab/backend/review/entrypoint/response/ActivityReviewResponse.kt +++ b/src/main/kotlin/picklab/backend/review/entrypoint/response/ActivityReviewResponse.kt @@ -26,6 +26,10 @@ data class ActivityReviewResponse( val participationDate: LocalDateTime, @field:Schema(description = "진행 상태 (진행 중 / 수료 완료 / 중도 포기)", example = "수료 완료") val progressStatus: ProgressStatus, + @field:Schema(description = "도움돼요 개수", example = "12") + val helpfulCount: Long, + @field:Schema(description = "현재 요청 사용자의 도움돼요 표시 여부 (비로그인 시 false)", example = "false") + val isHelpful: Boolean, // 로그인 사용자만 노출되는 필드 @field:Schema(description = "한줄평") val summary: String? = null, diff --git a/src/main/resources/db/migration/V1.11__drop_member_search_history_foreign_key.sql b/src/main/resources/db/migration/V1.11__drop_member_search_history_foreign_key.sql new file mode 100644 index 00000000..0b5adcc8 --- /dev/null +++ b/src/main/resources/db/migration/V1.11__drop_member_search_history_foreign_key.sql @@ -0,0 +1,2 @@ +ALTER TABLE member_search_history + DROP FOREIGN KEY fk_member_search_history_member_id; diff --git a/src/main/resources/db/migration/V1.12__create_review_helpful_table.sql b/src/main/resources/db/migration/V1.12__create_review_helpful_table.sql new file mode 100644 index 00000000..1836b28c --- /dev/null +++ b/src/main/resources/db/migration/V1.12__create_review_helpful_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS review_helpful +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '리뷰 도움돼요 ID', + member_id BIGINT NOT NULL COMMENT '회원 ID', + review_id BIGINT NOT NULL COMMENT '리뷰 ID', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + CONSTRAINT uk_review_helpful_member_review UNIQUE (member_id, review_id), + INDEX idx_review_helpful_review_id (review_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT ='리뷰 도움돼요 테이블'; diff --git a/src/test/kotlin/picklab/backend/review/ReviewHelpfulIntegrationTest.kt b/src/test/kotlin/picklab/backend/review/ReviewHelpfulIntegrationTest.kt new file mode 100644 index 00000000..dbca060e --- /dev/null +++ b/src/test/kotlin/picklab/backend/review/ReviewHelpfulIntegrationTest.kt @@ -0,0 +1,211 @@ +package picklab.backend.review + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.web.servlet.delete +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import picklab.backend.activity.domain.entity.Activity +import picklab.backend.activity.domain.entity.ExternalActivity +import picklab.backend.activity.domain.enums.ActivityFieldType +import picklab.backend.activity.domain.enums.LocationType +import picklab.backend.activity.domain.enums.OrganizerType +import picklab.backend.activity.domain.enums.ParticipantType +import picklab.backend.activity.domain.enums.RecruitmentStatus +import picklab.backend.activity.domain.repository.ActivityRepository +import picklab.backend.activitygroup.domain.entity.ActivityGroup +import picklab.backend.activitygroup.domain.repository.ActivityGroupRepository +import picklab.backend.common.model.SuccessCode +import picklab.backend.helper.WithMockUser +import picklab.backend.job.domain.JobCategoryRepository +import picklab.backend.job.domain.entity.JobCategory +import picklab.backend.job.domain.enums.JobGroup +import picklab.backend.member.domain.entity.Member +import picklab.backend.member.domain.repository.MemberRepository +import picklab.backend.participation.domain.entity.ActivityParticipation +import picklab.backend.participation.domain.enums.ApplicationStatus +import picklab.backend.participation.domain.enums.ProgressStatus +import picklab.backend.participation.domain.repository.ActivityParticipationRepository +import picklab.backend.review.domain.entity.Review +import picklab.backend.review.domain.entity.ReviewHelpful +import picklab.backend.review.domain.enums.ReviewApprovalStatus +import picklab.backend.review.domain.repository.ReviewHelpfulRepository +import picklab.backend.review.domain.repository.ReviewRepository +import picklab.backend.template.IntegrationTest +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +class ReviewHelpfulIntegrationTest : IntegrationTest() { + @Autowired + lateinit var memberRepository: MemberRepository + + @Autowired + lateinit var activityRepository: ActivityRepository + + @Autowired + lateinit var activityGroupRepository: ActivityGroupRepository + + @Autowired + lateinit var jobCategoryRepository: JobCategoryRepository + + @Autowired + lateinit var activityParticipationRepository: ActivityParticipationRepository + + @Autowired + lateinit var reviewRepository: ReviewRepository + + @Autowired + lateinit var reviewHelpfulRepository: ReviewHelpfulRepository + + lateinit var member: Member + lateinit var activity: Activity + lateinit var review: Review + + @BeforeEach + fun setUp() { + cleanUp.all() + + member = + memberRepository.save( + Member( + name = "테스트 유저", + email = "review-helpful@example.com", + ), + ) + val activityGroup = + activityGroupRepository.save( + ActivityGroup( + name = "테스트 그룹", + description = "테스트 그룹 설명", + ), + ) + activity = + activityRepository.save( + ExternalActivity( + title = "테스트 활동", + organizer = "테스트 기관", + organizerType = OrganizerType.PUBLIC_ORGANIZATION, + targetAudience = ParticipantType.WORKER, + location = LocationType.SEOUL_INCHEON, + recruitmentStartDate = LocalDate.now().minusMonths(6), + recruitmentEndDate = LocalDate.now().minusMonths(5), + startDate = LocalDate.now().minusMonths(4), + endDate = LocalDate.now().minusMonths(1), + status = RecruitmentStatus.CLOSED, + viewCount = 0L, + duration = + ChronoUnit.DAYS + .between(LocalDate.now().minusMonths(4), LocalDate.now().minusMonths(1)) + .toInt(), + activityHomepageUrl = null, + activityApplicationUrl = null, + activityThumbnailUrl = null, + description = null, + benefit = "테스트 혜택", + activityGroup = activityGroup, + activityField = ActivityFieldType.MENTORING, + ), + ) + val jobCategory = + jobCategoryRepository.save( + JobCategory( + jobGroup = JobGroup.DEVELOPMENT, + ), + ) + activityParticipationRepository.save( + ActivityParticipation( + applicationStatus = ApplicationStatus.ACCEPTED, + progressStatus = ProgressStatus.COMPLETED, + member = member, + activity = activity, + ), + ) + review = + reviewRepository.save( + Review( + overallScore = 5, + infoScore = 4, + difficultyScore = 3, + benefitScore = 5, + summary = "도움되는 리뷰", + strength = "장점", + weakness = "단점", + tips = "팁", + jobRelevanceScore = 5, + reviewApprovalStatus = ReviewApprovalStatus.APPROVED, + member = member, + activity = activity, + jobCategory = jobCategory, + ), + ) + } + + @Test + @WithMockUser + @DisplayName("도움돼요 생성은 멱등하며 로그인 리뷰 목록에 선택 상태를 반환한다") + fun markReviewHelpful() { + repeat(2) { + mockMvc + .post("/v1/reviews/${review.id}/helpful") + .andExpect { + status { isOk() } + jsonPath("$.message") { value(SuccessCode.MARK_REVIEW_HELPFUL_SUCCESS.message) } + } + } + + assertThat(reviewHelpfulRepository.count()).isEqualTo(1) + + mockMvc + .get("/v1/activities/${activity.id}/reviews") + .andExpect { + status { isOk() } + jsonPath("$.data.items[0].helpful_count") { value(1) } + jsonPath("$.data.items[0].is_helpful") { value(true) } + } + } + + @Test + @DisplayName("비로그인 리뷰 목록에는 도움돼요 개수와 false 상태를 반환한다") + fun getReviewHelpfulAnonymously() { + reviewHelpfulRepository.save( + ReviewHelpful( + member = member, + review = review, + ), + ) + + mockMvc + .get("/v1/activities/${activity.id}/reviews") + .andExpect { + status { isOk() } + jsonPath("$.data.items[0].helpful_count") { value(1) } + jsonPath("$.data.items[0].is_helpful") { value(false) } + } + } + + @Test + @WithMockUser + @DisplayName("도움돼요 취소는 멱등하게 hard delete한다") + fun unmarkReviewHelpful() { + reviewHelpfulRepository.save( + ReviewHelpful( + member = member, + review = review, + ), + ) + + repeat(2) { + mockMvc + .delete("/v1/reviews/${review.id}/helpful") + .andExpect { + status { isOk() } + jsonPath("$.message") { value(SuccessCode.UNMARK_REVIEW_HELPFUL_SUCCESS.message) } + } + } + + assertThat(reviewHelpfulRepository.count()).isZero() + } +} diff --git a/src/test/kotlin/picklab/backend/review/application/UpdateReviewUnitTest.kt b/src/test/kotlin/picklab/backend/review/application/UpdateReviewUnitTest.kt index e1e242e3..9144aabd 100644 --- a/src/test/kotlin/picklab/backend/review/application/UpdateReviewUnitTest.kt +++ b/src/test/kotlin/picklab/backend/review/application/UpdateReviewUnitTest.kt @@ -21,6 +21,7 @@ import picklab.backend.participation.domain.service.ActivityParticipationService import picklab.backend.review.application.model.ReviewUpdateCommand import picklab.backend.review.application.service.ReviewOverviewQueryService import picklab.backend.review.domain.entity.Review +import picklab.backend.review.domain.service.ReviewHelpfulService import picklab.backend.review.domain.service.ReviewService import kotlin.test.Test @@ -44,6 +45,9 @@ class UpdateReviewUnitTest { @MockK lateinit var jobService: JobService + @MockK + lateinit var reviewHelpfulService: ReviewHelpfulService + @InjectMockKs lateinit var reviewUseCase: ReviewUseCase