Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/kotlin/picklab/backend/common/model/SuccessCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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, "활동별 만족도 통계 조회에 성공했습니다."),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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<ActivityReviewListView> {
): Page<ActivityReviewWithHelpful> {
memberId?.let { memberService.findActiveMember(it) }
val activity = activityService.mustFindById(activityId)
val pageable =
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package picklab.backend.review.domain.repository

interface ReviewHelpfulCountProjection {
val reviewId: Long
val helpfulCount: Long
}
Original file line number Diff line number Diff line change
@@ -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<ReviewHelpful, Long> {
@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<Long>,
): List<ReviewHelpfulCountProjection>

@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<Long>,
): List<Long>
}
Original file line number Diff line number Diff line change
@@ -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<Long>): Map<Long, Long> {
if (reviewIds.isEmpty()) {
return emptyMap()
}

return reviewHelpfulRepository
.countByReviewIds(reviewIds)
.associate { it.reviewId to it.helpfulCount }
}

fun findHelpfulReviewIds(
memberId: Long?,
reviewIds: Collection<Long>,
): Set<Long> {
if (memberId == null || reviewIds.isEmpty()) {
return emptySet()
}

return reviewHelpfulRepository
.findReviewIdsByMemberId(memberId, reviewIds)
.toSet()
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/picklab/backend/review/entrypoint/ReviewApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ interface ReviewApi {
@ParameterObject request: ActivityReviewListRequest,
): ResponseEntity<ResponseWrapper<PageResponse<ActivityReviewResponse>>>

@Operation(
summary = "리뷰 도움돼요 표시",
description = "로그인한 사용자가 리뷰에 도움돼요를 표시합니다. 이미 표시한 경우에도 성공합니다.",
)
fun markReviewHelpful(
@Parameter(description = "리뷰 ID값") @PathVariable id: Long,
member: MemberPrincipal,
): ResponseEntity<ResponseWrapper<Unit>>

@Operation(
summary = "리뷰 도움돼요 취소",
description = "로그인한 사용자가 리뷰의 도움돼요 표시를 취소합니다. 표시하지 않은 경우에도 성공합니다.",
)
fun unmarkReviewHelpful(
@Parameter(description = "리뷰 ID값") @PathVariable id: Long,
member: MemberPrincipal,
): ResponseEntity<ResponseWrapper<Unit>>

@Operation(
summary = "리뷰 수정",
description = "본인이 작성한 리뷰를 수정합니다.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResponseWrapper<Unit>> {
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<ResponseWrapper<Unit>> {
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE member_search_history
DROP FOREIGN KEY fk_member_search_history_member_id;
Original file line number Diff line number Diff line change
@@ -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 ='리뷰 도움돼요 테이블';
Loading
Loading