Skip to content

Commit 11d6408

Browse files
authored
Feat: 멘토링 리뷰 구현 (#140)
* Chore: 리뷰 엔티티 작성자 필드, 인덱스 추가 * Feat: 리뷰 생성 * Feat: 리뷰 수정 * Feat: 리뷰 삭제 * Feat: 리뷰 조회 * Feat: 리뷰 목록 조회 * Fix: getId 로 equals
1 parent 69b848b commit 11d6408

File tree

12 files changed

+777
-0
lines changed

12 files changed

+777
-0
lines changed

back/src/main/java/com/back/domain/member/mentor/entity/Mentor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public void updateCareerYears(Integer careerYears) {
4040
this.careerYears = careerYears;
4141
}
4242

43+
public void updateRating(Double averageRating) {
44+
this.rate = averageRating;
45+
}
46+
4347
public void delete() {
4448
this.isDeleted = true;
4549
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.back.domain.mentoring.mentoring.controller;
2+
3+
import com.back.domain.member.member.service.MemberStorage;
4+
import com.back.domain.member.mentee.entity.Mentee;
5+
import com.back.domain.mentoring.mentoring.dto.request.ReviewRequest;
6+
import com.back.domain.mentoring.mentoring.dto.response.ReviewPagingResponse;
7+
import com.back.domain.mentoring.mentoring.dto.response.ReviewResponse;
8+
import com.back.domain.mentoring.mentoring.service.ReviewService;
9+
import com.back.global.rq.Rq;
10+
import com.back.global.rsData.RsData;
11+
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.tags.Tag;
13+
import jakarta.validation.Valid;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.data.domain.Page;
16+
import org.springframework.security.access.prepost.PreAuthorize;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
@RestController
20+
@RequiredArgsConstructor
21+
@Tag(name = "ReviewController", description = "멘토링 리뷰 API")
22+
public class ReviewController {
23+
private final Rq rq;
24+
private final MemberStorage memberStorage;
25+
private final ReviewService reviewService;
26+
27+
@GetMapping("/mentorings/{mentoringId}/reviews")
28+
@Operation(summary = "멘토링 리뷰 조회", description = "멘토링 리뷰 목록을 조회합니다.")
29+
public RsData<ReviewPagingResponse> getReviews(
30+
@PathVariable Long mentoringId,
31+
@RequestParam(defaultValue = "0") int page,
32+
@RequestParam(defaultValue = "10") int size
33+
) {
34+
Page<ReviewResponse> reviewPage = reviewService.getReviews(mentoringId, page, size);
35+
ReviewPagingResponse resDto = ReviewPagingResponse.from(reviewPage);
36+
37+
return new RsData<>(
38+
"200",
39+
"멘토링 리뷰 목록을 조회하였습니다.",
40+
resDto
41+
);
42+
}
43+
44+
@GetMapping("/reviews/{reviewId}")
45+
@Operation(summary = "멘토링 리뷰 조회", description = "멘토링 리뷰를 조회합니다.")
46+
public RsData<ReviewResponse> getReview(
47+
@PathVariable Long reviewId
48+
) {
49+
ReviewResponse resDto = reviewService.getReview(reviewId);
50+
51+
return new RsData<>(
52+
"200",
53+
"멘토링 리뷰를 조회하였습니다.",
54+
resDto
55+
);
56+
}
57+
58+
@PostMapping("/reservations/{reservationId}/reviews")
59+
@PreAuthorize("hasRole('MENTEE')")
60+
@Operation(summary = "멘토링 리뷰 작성", description = "멘토링 리뷰를 작성합니다.")
61+
public RsData<ReviewResponse> createReview(
62+
@PathVariable Long reservationId,
63+
@RequestBody @Valid ReviewRequest reqDto
64+
) {
65+
Mentee mentee = memberStorage.findMenteeByMember(rq.getActor());
66+
ReviewResponse resDto = reviewService.createReview(reservationId, reqDto, mentee);
67+
68+
return new RsData<>(
69+
"201",
70+
"멘토링 리뷰가 작성되었습니다.",
71+
resDto
72+
);
73+
}
74+
75+
@PutMapping("/reviews/{reviewId}")
76+
@PreAuthorize("hasRole('MENTEE')")
77+
@Operation(summary = "멘토링 리뷰 수정", description = "멘토링 리뷰를 수정합니다.")
78+
public RsData<ReviewResponse> updateReview(
79+
@PathVariable Long reviewId,
80+
@RequestBody @Valid ReviewRequest reqDto
81+
) {
82+
Mentee mentee = memberStorage.findMenteeByMember(rq.getActor());
83+
ReviewResponse resDto = reviewService.updateReview(reviewId, reqDto, mentee);
84+
85+
return new RsData<>(
86+
"200",
87+
"멘토링 리뷰가 수정되었습니다.",
88+
resDto
89+
);
90+
}
91+
92+
@DeleteMapping("/reviews/{reviewId}")
93+
@PreAuthorize("hasRole('MENTEE')")
94+
@Operation(summary = "멘토링 리뷰 삭제", description = "멘토링 리뷰를 삭제합니다.")
95+
public RsData<Void> deleteReview(
96+
@PathVariable Long reviewId
97+
) {
98+
Mentee mentee = memberStorage.findMenteeByMember(rq.getActor());
99+
reviewService.deleteReview(reviewId, mentee);
100+
101+
return new RsData<>(
102+
"200",
103+
"멘토링 리뷰가 삭제되었습니다."
104+
);
105+
}
106+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.back.domain.mentoring.mentoring.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.*;
5+
6+
public record ReviewRequest(
7+
@NotNull
8+
@DecimalMin(value = "0.0")
9+
@DecimalMax(value = "5.0")
10+
@Schema(description = "별점 (0.0 ~ 5.0)", example = "4.5")
11+
Double rating,
12+
13+
@Size(max = 1000)
14+
@Schema(description = "리뷰 내용")
15+
String content
16+
) {
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.back.domain.mentoring.mentoring.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import org.springframework.data.domain.Page;
5+
6+
import java.util.List;
7+
8+
public record ReviewPagingResponse(
9+
List<ReviewResponse> reviews,
10+
@Schema(description = "현재 페이지 (0부터 시작)")
11+
int currentPage,
12+
@Schema(description = "총 페이지")
13+
int totalPage,
14+
@Schema(description = "총 개수")
15+
long totalElements,
16+
@Schema(description = "다음 페이지 존재 여부")
17+
boolean hasNext
18+
) {
19+
public static ReviewPagingResponse from(Page<ReviewResponse> page) {
20+
return new ReviewPagingResponse(
21+
page.getContent(),
22+
page.getNumber(),
23+
page.getTotalPages(),
24+
page.getTotalElements(),
25+
page.hasNext()
26+
);
27+
}
28+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.back.domain.mentoring.mentoring.dto.response;
2+
3+
import com.back.domain.mentoring.mentoring.entity.Review;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
6+
import java.time.LocalDateTime;
7+
8+
public record ReviewResponse(
9+
@Schema(description = "리뷰 ID")
10+
Long reviewId,
11+
@Schema(description = "별점 (0.0 ~ 5.0)")
12+
Double rating,
13+
@Schema(description = "리뷰 내용")
14+
String content,
15+
@Schema(description = "생성일")
16+
LocalDateTime createDate,
17+
@Schema(description = "수정일")
18+
LocalDateTime modifyDate,
19+
20+
@Schema(description = "멘티 ID")
21+
Long menteeId,
22+
@Schema(description = "멘티 닉네임")
23+
String menteeNickname
24+
) {
25+
public static ReviewResponse from(Review review) {
26+
return new ReviewResponse(
27+
review.getId(),
28+
review.getRating(),
29+
review.getContent(),
30+
review.getCreateDate(),
31+
review.getModifyDate(),
32+
review.getMentee().getId(),
33+
review.getMentee().getMember().getNickname()
34+
);
35+
}
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,65 @@
11
package com.back.domain.mentoring.mentoring.entity;
22

3+
import com.back.domain.member.mentee.entity.Mentee;
4+
import com.back.domain.mentoring.mentoring.error.ReviewErrorCode;
35
import com.back.domain.mentoring.reservation.entity.Reservation;
6+
import com.back.global.exception.ServiceException;
47
import com.back.global.jpa.BaseEntity;
58
import jakarta.persistence.*;
9+
import lombok.Builder;
610
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
712

813
@Entity
14+
@Table(
15+
name = "review",
16+
indexes = {
17+
@Index(name = "idx_review_mentee", columnList = "mentee_id"),
18+
@Index(name = "idx_review_reservation", columnList = "reservation_id")
19+
}
20+
)
921
@Getter
22+
@NoArgsConstructor
1023
public class Review extends BaseEntity {
1124
@ManyToOne(fetch = FetchType.LAZY)
1225
@JoinColumn(name = "reservation_id", nullable = false)
1326
private Reservation reservation;
1427

28+
@ManyToOne(fetch = FetchType.LAZY)
29+
@JoinColumn(name = "mentee_id", nullable = false)
30+
private Mentee mentee;
31+
1532
@Column(nullable = false)
1633
private double rating;
1734

1835
@Column(columnDefinition = "TEXT")
1936
private String content;
37+
38+
@Builder
39+
public Review(Reservation reservation, Mentee mentee, double rating, String content) {
40+
ensureMentee(reservation, mentee);
41+
42+
this.reservation = reservation;
43+
this.mentee = mentee;
44+
this.rating = rating;
45+
this.content = content;
46+
}
47+
48+
public void update(double rating, String content) {
49+
this.rating = rating;
50+
this.content = content;
51+
}
52+
53+
public boolean isMentee(Mentee mentee) {
54+
return mentee.getId().equals(this.mentee.getId());
55+
}
56+
57+
58+
// ===== 헬퍼 메서드 =====
59+
60+
private void ensureMentee(Reservation reservation, Mentee mentee) {
61+
if (!reservation.isMentee(mentee)) {
62+
throw new ServiceException(ReviewErrorCode.FORBIDDEN_NOT_MENTEE);
63+
}
64+
}
2065
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.back.domain.mentoring.mentoring.error;
2+
3+
import com.back.global.exception.ErrorCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public enum ReviewErrorCode implements ErrorCode {
10+
11+
// 400
12+
CANNOT_REVIEW("400-1", "멘토링 진행 전입니다. 리뷰를 작성할 수 없습니다."),
13+
INVALID_RATING_UNIT("400-2", "평점은 0.5 단위로 입력해야 합니다."),
14+
15+
// 403
16+
FORBIDDEN_NOT_MENTEE("403-1", "예약의 멘티만 리뷰를 작성할 수 있습니다."),
17+
18+
// 404
19+
REVIEW_NOT_FOUND("404-1", "리뷰를 찾을 수 없습니다."),
20+
21+
// 409
22+
ALREADY_EXISTS_REVIEW("409-1", "이미 리뷰가 존재합니다.");
23+
24+
private final String code;
25+
private final String message;
26+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
11
package com.back.domain.mentoring.mentoring.repository;
22

3+
import com.back.domain.member.mentor.entity.Mentor;
34
import com.back.domain.mentoring.mentoring.entity.Review;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
47
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
510

611
public interface ReviewRepository extends JpaRepository<Review, Long> {
12+
boolean existsByReservationId(Long id);
13+
14+
@Query("""
15+
SELECT AVG(r.rating)
16+
FROM Review r
17+
INNER JOIN r.reservation res
18+
WHERE res.mentor = :mentor
19+
""")
20+
Double findAverageRating(
21+
@Param("mentor") Mentor mentor
22+
);
23+
24+
@Query("""
25+
SELECT r
26+
FROM Review r
27+
WHERE r.reservation.mentoring.id = :mentoringId
28+
ORDER BY r.createDate DESC
29+
""")
30+
Page<Review> findAllByMentoringId(
31+
@Param("mentoringId") Long mentoringId,
32+
Pageable pageable
33+
);
734
}

0 commit comments

Comments
 (0)