Skip to content

Commit ede7eda

Browse files
authored
Merge pull request #57 from GAT2025/feat/#56
[Feat/#56] 리뷰 수정&삭제 기능 구현
2 parents 2c0b96c + eb39c42 commit ede7eda

14 files changed

Lines changed: 176 additions & 28 deletions

File tree

src/main/kotlin/gat_be/dev/common/status/ErrorStatus.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ enum class ErrorStatus(
8585
/**
8686
* Review
8787
*/
88-
REVIEW_ALREADY_EXISTS(HttpStatus.CONFLICT, "REVIEW_409", "리뷰를 이미 작성하였습니다"),
89-
TEAM_NOT_MATCHES_WITH_REVIEWEE(HttpStatus.FORBIDDEN, "REVIEW_403", "해당 유저와 같은 팀이 아닙니다"),
90-
CANNOT_CREATE_REVIEW_OF_MYSELF(HttpStatus.CONFLICT, "REVIEW_409", "본인에 대한 리뷰는 작성할 수 없습니다"),
91-
CANNOT_CREATE_REVIEW_WHEN_TEAM_IS_NOT_DONE(HttpStatus.CONFLICT, "REVIEW_409", "팀 활동이 종료되기 전에는 리뷰를 작성할 수 없습니다"),
9288
INVALID_REVIEW_LIST_SORT_PARAM(HttpStatus.BAD_REQUEST, "REVIEW_400", "리뷰 리스트 조회를 위한 정렬 조건이 잘못 되었습니다."),
93-
REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW_404", "해당 리뷰 정보를 찾을 수 없습니다"),
89+
TEAM_NOT_MATCHES_WITH_REVIEWEE(HttpStatus.FORBIDDEN, "REVIEW_403", "해당 유저와 같은 팀이 아닙니다"),
90+
REVIEW_FORBIDDEN(HttpStatus.FORBIDDEN, "REVIEW_403", "리뷰에 대한 접근 권한이 없습니다."),
91+
REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW_404", "해당 리뷰 정보를 찾을 수 없습니다."),
92+
REVIEW_ALREADY_EXISTS(HttpStatus.CONFLICT, "REVIEW_409", "리뷰를 이미 작성하였습니다."),
93+
CANNOT_CREATE_REVIEW_OF_MYSELF(HttpStatus.CONFLICT, "REVIEW_409", "본인에 대한 리뷰는 작성할 수 없습니다."),
94+
CANNOT_CREATE_REVIEW_WHEN_TEAM_IS_NOT_DONE(HttpStatus.CONFLICT, "REVIEW_409", "팀 활동이 종료되기 전에는 리뷰를 작성할 수 없습니다."),
9495

9596
/**
9697
* Notification

src/main/kotlin/gat_be/dev/common/status/SuccessStatus.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,7 @@ enum class SuccessStatus(
7272
GET_REVIEW_LIST_SUCCESS(HttpStatus.OK, "REVIEW_200", "리뷰 리스트 조회 성공"),
7373
GET_REVIEW_DETAIL_SUCCESS(HttpStatus.OK, "REVIEW_200", "리뷰 상세 조회 성공"),
7474
GET_REVIEW_EXISTS_SUCCESS(HttpStatus.OK, "REVIEW_200", "리뷰 작성 여부 조회 성공"),
75+
UPDATE_REVIEW_SUCCESS(HttpStatus.OK, "REVIEW_200", "리뷰 수정 성공"),
76+
DELETE_REVIEW_SUCCESS(HttpStatus.OK, "REVIEW_200", "리뷰 삭제 성공")
77+
7578
}

src/main/kotlin/gat_be/dev/domain/review/controller/ReviewController.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@ package gat_be.dev.domain.review.controller
33
import ApiResponse
44
import gat_be.dev.common.status.SuccessStatus
55
import gat_be.dev.domain.review.dto.request.CreateReviewRequest
6+
import gat_be.dev.domain.review.dto.request.UpdateReviewRequest
67
import gat_be.dev.domain.review.dto.response.ReviewDetailResponse
78
import gat_be.dev.domain.review.dto.response.ReviewExistsResponse
89
import gat_be.dev.domain.review.dto.response.ReviewListResponse
910
import gat_be.dev.domain.review.facade.ReviewCreateFacade
1011
import gat_be.dev.domain.review.facade.ReviewDetailGetFacade
1112
import gat_be.dev.domain.review.facade.ReviewExistsGetFacade
1213
import gat_be.dev.domain.review.facade.ReviewListGetFacade
14+
import gat_be.dev.domain.review.service.ReviewService
1315
import io.swagger.v3.oas.annotations.Operation
1416
import io.swagger.v3.oas.annotations.media.Content
1517
import io.swagger.v3.oas.annotations.media.Schema
1618
import io.swagger.v3.oas.annotations.tags.Tag
1719
import jakarta.validation.Valid
1820
import org.springframework.http.ResponseEntity
1921
import org.springframework.security.core.annotation.AuthenticationPrincipal
22+
import org.springframework.web.bind.annotation.DeleteMapping
2023
import org.springframework.web.bind.annotation.GetMapping
24+
import org.springframework.web.bind.annotation.PatchMapping
2125
import org.springframework.web.bind.annotation.PathVariable
2226
import org.springframework.web.bind.annotation.PostMapping
2327
import org.springframework.web.bind.annotation.RequestBody
@@ -32,7 +36,8 @@ class ReviewController(
3236
private val reviewCreateFacade: ReviewCreateFacade,
3337
private val reviewListGetFacade: ReviewListGetFacade,
3438
private val reviewDetailGetFacade: ReviewDetailGetFacade,
35-
private val reviewExistsGetFacade: ReviewExistsGetFacade
39+
private val reviewExistsGetFacade: ReviewExistsGetFacade,
40+
private val reviewService: ReviewService
3641
) {
3742
@PostMapping("")
3843
@Operation(summary = "새로운 리뷰 작성")
@@ -84,4 +89,33 @@ class ReviewController(
8489
val reviewExistsResponse = reviewExistsGetFacade.getReviewExists(userId, teamId)
8590
return ApiResponse.success(SuccessStatus.GET_REVIEW_EXISTS_SUCCESS, reviewExistsResponse)
8691
}
92+
93+
@PatchMapping("/{reviewId}")
94+
@Operation(summary = "리뷰 수정")
95+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "리뷰 수정 성공", content = [Content()])
96+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "리뷰 수정을 위한 필드가 잘못 되었을 경우", content = [Content()])
97+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "리뷰 수정을 위한 권한이 없을 경우", content = [Content()])
98+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "리뷰가 존재하지 않을 경우", content = [Content()])
99+
fun updateReview(
100+
@AuthenticationPrincipal userId: Long,
101+
@PathVariable reviewId: Long,
102+
@RequestBody request: UpdateReviewRequest
103+
): ResponseEntity<ApiResponse<Nothing>> {
104+
reviewService.updateReview(userId, reviewId, request)
105+
return ApiResponse.success(SuccessStatus.UPDATE_REVIEW_SUCCESS)
106+
}
107+
108+
@DeleteMapping("/{reviewId}")
109+
@Operation(summary = "리뷰 삭제")
110+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "리뷰 삭제 성공", content = [Content()])
111+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "리뷰 삭제를 위한 권한이 없을 경우", content = [Content()])
112+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "리뷰가 존재하지 않을 경우", content = [Content()])
113+
fun deleteReview(
114+
@AuthenticationPrincipal userId: Long,
115+
@PathVariable reviewId: Long
116+
): ResponseEntity<ApiResponse<Nothing>> {
117+
reviewService.deleteReview(userId, reviewId)
118+
return ApiResponse.success(SuccessStatus.DELETE_REVIEW_SUCCESS)
119+
}
120+
87121
}

src/main/kotlin/gat_be/dev/domain/review/dto/request/ReviewRequest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,29 @@ data class CreateReviewRequest(
3434
@field:NotBlank(message = "리뷰 내용을 입력해주세요.")
3535
@field:Size(max = 200, message = "내용은 200자를 초과할 수 없습니다.")
3636
val content: String
37+
)
38+
39+
data class UpdateReviewRequest(
40+
@field:Min(value = 1, message = "점수는 최소 1점입니다.")
41+
@field:Max(value = 5, message = "점수는 최대 5점입니다.")
42+
val timePunctualityScore: Int?,
43+
44+
@field:Min(value = 1, message = "점수는 최소 1점입니다.")
45+
@field:Max(value = 5, message = "점수는 최대 5점입니다.")
46+
val communicationScore: Int?,
47+
48+
@field:Min(value = 1, message = "점수는 최소 1점입니다.")
49+
@field:Max(value = 5, message = "점수는 최대 5점입니다.")
50+
val skillAndContributionScore: Int?,
51+
52+
@field:Min(value = 1, message = "점수는 최소 1점입니다.")
53+
@field:Max(value = 5, message = "점수는 최대 5점입니다.")
54+
val collaborationAndTeamworkScore: Int?,
55+
56+
@field:Min(value = 1, message = "점수는 최소 1점입니다.")
57+
@field:Max(value = 5, message = "점수는 최대 5점입니다.")
58+
val rating: Double?,
59+
60+
@field:Size(max = 200, message = "내용은 200자를 초과할 수 없습니다.")
61+
val content: String?
3762
)

src/main/kotlin/gat_be/dev/domain/review/entity/Review.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gat_be.dev.domain.review.entity
22

33
import gat_be.dev.common.base.BaseEntity
4+
import gat_be.dev.common.exception.GeneralException
5+
import gat_be.dev.common.status.ErrorStatus
46
import gat_be.dev.domain.review.enums.Category
57
import jakarta.persistence.*
68

@@ -35,7 +37,14 @@ class Review(
3537
var categories: MutableList<ReviewCategory> = mutableListOf()
3638

3739
// 3. Business logic
38-
fun getCategoryScore(category: Category): Int {
39-
return categories.find { it.category == category }?.score ?: 0
40+
fun updateRating(rating: Double) { this.rating = rating }
41+
42+
fun updateContent(content: String) { this.content = content }
43+
44+
fun validateReviewer(userId: Long) {
45+
if (this.userId != userId) {
46+
throw GeneralException(ErrorStatus.REVIEW_FORBIDDEN)
47+
}
4048
}
49+
4150
}

src/main/kotlin/gat_be/dev/domain/review/entity/ReviewCategory.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ class ReviewCategory(
2424
private val reviewCategoryId: Long = 0L
2525

2626
@Column(nullable = false)
27-
var score: Int = score // TODO: 1 ~ 5 값 제한 -> valid 추가 or enum으로 변경
27+
var score: Int = score
2828
protected set
2929

30+
// 2. Business Logic
31+
fun updateScore(score: Int) { this.score = score }
32+
3033
}

src/main/kotlin/gat_be/dev/domain/review/facade/ReviewCreateFacade.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import gat_be.dev.domain.review.service.ReviewService
77
import gat_be.dev.domain.team.entity.Team
88
import gat_be.dev.domain.team.entity.TeamMember
99
import gat_be.dev.domain.team.service.TeamService
10+
import gat_be.dev.domain.user.service.UserService
1011
import jakarta.transaction.Transactional
1112
import org.springframework.stereotype.Service
1213

1314
@Service
1415
class ReviewCreateFacade(
1516
private val reviewService: ReviewService,
16-
private val teamService: TeamService
17+
private val teamService: TeamService,
18+
private val userService: UserService
1719
) {
1820
// 리뷰 작성하기
1921
@Transactional
@@ -27,6 +29,10 @@ class ReviewCreateFacade(
2729
reviewService.validateDuplicateReview(reviewer.userId, reviewee.teamMemberId)
2830

2931
reviewService.createReview(userId, request)
32+
33+
val reviewListForReviewee = reviewService.findReviewListByTeamMemberId(reviewee.teamMemberId)
34+
val newRating = reviewListForReviewee.map { it.rating }.average()
35+
userService.updateRating(reviewee.userId, newRating)
3036
}
3137

3238
private fun validateIsTeamDone(targetTeam: Team) {

src/main/kotlin/gat_be/dev/domain/review/repository/ReviewCategoryRepository.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import org.springframework.data.repository.query.Param
99
interface ReviewCategoryRepository: JpaRepository<ReviewCategory, Long> {
1010

1111
@Modifying
12-
@Query("""
13-
DELETE FROM ReviewCategory rc
14-
WHERE rc.review.teamMemberId = :teamMemberId
15-
""")
12+
@Query("DELETE FROM ReviewCategory rc " +
13+
"WHERE rc.review.teamMemberId = :teamMemberId")
1614
fun deleteAllByTeamMemberId(@Param("teamMemberId") teamMemberId: Long)
1715

16+
@Modifying
17+
@Query("DELETE FROM ReviewCategory rc " +
18+
"WHERE rc.review.reviewId = :reviewId")
19+
fun deleteAllByReviewId(@Param("reviewId") reviewId: Long)
20+
1821
}

src/main/kotlin/gat_be/dev/domain/review/repository/ReviewRepository.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,26 @@ import org.springframework.data.jpa.repository.Query
99
import org.springframework.data.repository.query.Param
1010

1111
interface ReviewRepository: JpaRepository<Review, Long> {
12-
@Query("SELECT r FROM Review r JOIN FETCH r.categories WHERE r.teamMemberId = :teamMemberId")
12+
@Query("SELECT r FROM Review r " +
13+
"JOIN FETCH r.categories " +
14+
"WHERE r.teamMemberId = :teamMemberId")
1315
fun findAllWithCategoriesByTeamMemberId(@Param("teamMemberId") teamMemberId: Long): List<Review>
1416

17+
@Query("SELECT r FROM Review r " +
18+
"JOIN FETCH r.categories " +
19+
"WHERE r.teamMemberId IN :teamMemberIdList")
20+
fun findAllWithCategoriesByTeamMemberIdList(@Param("teamMemberIdList") teamMemberIdList: List<Long>): List<Review>
21+
1522
@Modifying
16-
@Query("""
17-
DELETE FROM Review r
18-
WHERE r.teamMemberId = :teamMemberId
19-
""")
23+
@Query("DELETE FROM Review r " +
24+
"WHERE r.teamMemberId = :teamMemberId")
2025
fun deleteAllByTeamMemberId(@Param("teamMemberId") teamMemberId: Long)
2126

27+
@Modifying
28+
@Query("DELETE FROM Review r " +
29+
"WHERE r.reviewId = :reviewId")
30+
fun deleteByReviewId(@Param("reviewId") reviewId: Long)
31+
2232
fun existsByUserIdAndTeamMemberId(userId: Long, teamMemberId: Long): Boolean
2333

2434
@Query("SELECT new gat_be.dev.domain.review.dto.response.ReviewInfo(r, t.title) " +

src/main/kotlin/gat_be/dev/domain/review/service/ReviewService.kt

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gat_be.dev.domain.review.service
33
import gat_be.dev.common.exception.GeneralException
44
import gat_be.dev.common.status.ErrorStatus
55
import gat_be.dev.domain.review.dto.request.CreateReviewRequest
6+
import gat_be.dev.domain.review.dto.request.UpdateReviewRequest
67
import gat_be.dev.domain.review.dto.response.ReviewInfo
78
import gat_be.dev.domain.review.entity.Review
89
import gat_be.dev.domain.review.entity.ReviewCategory
@@ -45,16 +46,47 @@ class ReviewService(
4546
)
4647
}
4748

49+
// 리뷰 수정
50+
@Transactional
51+
fun updateReview(userId: Long, reviewId: Long, request: UpdateReviewRequest) {
52+
val review = findReviewByReviewId(reviewId)
53+
review.validateReviewer(userId)
54+
55+
review.run {
56+
request.rating?.let(::updateRating)
57+
request.content?.let(::updateContent)
58+
}
59+
60+
val categoryMap = review.categories.associateBy { it.category }
61+
62+
request.timePunctualityScore?.let { categoryMap[Category.TIME_PUNCTUALITY]?.updateScore(it) }
63+
request.communicationScore?.let { categoryMap[Category.COMMUNICATION]?.updateScore(it) }
64+
request.skillAndContributionScore?.let { categoryMap[Category.SKILL_AND_CONTRIBUTION]?.updateScore(it) }
65+
request.collaborationAndTeamworkScore?.let { categoryMap[Category.COLLABORATION_AND_TEAMWORK]?.updateScore(it) }
66+
}
67+
68+
// 리뷰 삭제
69+
@Transactional
70+
fun deleteReview(userId: Long, reviewId: Long) {
71+
val review = findReviewByReviewId(reviewId)
72+
review.validateReviewer(userId)
73+
74+
reviewCategoryRepository.deleteAllByReviewId(review.reviewId)
75+
reviewRepository.deleteByReviewId(review.reviewId)
76+
77+
// TODO: reviewee rating 재계산
78+
}
79+
4880
fun validateDuplicateReview(userId: Long, teamMemberId: Long) {
4981
if (reviewRepository.existsByUserIdAndTeamMemberId(userId, teamMemberId)) {
5082
throw GeneralException(ErrorStatus.REVIEW_ALREADY_EXISTS)
5183
}
5284
}
5385

5486
// 유저의 리뷰 카테고리별 개수 집계
55-
fun getUserReviewDetails(user: User): UserReviewDetails {
87+
fun getUserReviewDetails(user: User, teamMemberIdList: List<Long>): UserReviewDetails {
5688
// 1. 사용자 리뷰 목록 조회
57-
val reviewList = reviewRepository.findAllWithCategoriesByTeamMemberId(user.userId)
89+
val reviewList = findReviewListByTeamMemberIdList(teamMemberIdList)
5890

5991
// 2. 카테고리별 점수 리스트 구성
6092
val categoryScoreMap = mutableMapOf<Category, MutableList<Int>>()
@@ -85,6 +117,14 @@ class ReviewService(
85117
)
86118
}
87119

120+
fun findReviewListByTeamMemberIdList(teamMemberIdList: List<Long>): List<Review> {
121+
return reviewRepository.findAllWithCategoriesByTeamMemberIdList(teamMemberIdList)
122+
}
123+
124+
fun findReviewListByTeamMemberId(teamMemberId: Long): List<Review> {
125+
return reviewRepository.findAllWithCategoriesByTeamMemberId(teamMemberId)
126+
}
127+
88128
// 유저에 대해 작성된 리뷰 삭제
89129
fun deleteAllByUserId(userId: Long) {
90130
reviewCategoryRepository.deleteAllByTeamMemberId(userId)
@@ -95,7 +135,7 @@ class ReviewService(
95135
@Transactional
96136
fun getReviewInfoList(teamMemberIdList: List<Long>, sortBy: String, order: String): List<ReviewInfo> {
97137
val sortOrder = getSort(sortBy, order)
98-
return findReviewListByTeamMemberId(teamMemberIdList, sortOrder)
138+
return findReviewListByTeamMemberIdList(teamMemberIdList, sortOrder)
99139
}
100140

101141
private fun getSort(sort: String, order: String): Sort {
@@ -112,7 +152,7 @@ class ReviewService(
112152
}
113153
}
114154

115-
private fun findReviewListByTeamMemberId(teamMemberIdList: List<Long>, sortOrder: Sort): List<ReviewInfo> {
155+
private fun findReviewListByTeamMemberIdList(teamMemberIdList: List<Long>, sortOrder: Sort): List<ReviewInfo> {
116156
return reviewRepository.findReviewListByTeamMemberIdList(teamMemberIdList, sortOrder)
117157
}
118158

0 commit comments

Comments
 (0)