Skip to content

Commit 94501af

Browse files
authored
[feature] 코스 리뷰 전체 보기 (#150)
* feat: 코스리뷰 전체 보기 쿼리 설계 * feat: 코스리뷰 응답 구조 설계 * feat: 코스리뷰 전체보기 서비스 구현 * feat: 코스리뷰 전체보기 api 설계 및 코스 평점 타입 수정 * fix: 코스리뷰 전체보기 쿼리 수정 * fix: 응답 구조 수정 * fix: distinct로 중복 데이터 방지 * feat: 코스리뷰에 리뷰 방문 시간 추가 * fix: 코스리뷰 조회하기 쿼리 수정 * fix: 방문 일자 데이터 타입 변경 LocalDateTime -> LocalDate * fix: 코스 리뷰 전체 조회 쿼리 수정 (N+1 문제 해결) * fix: LocalDate 로 수정 * fix: 쿼리 수정 및 작성된 리뷰 없을 시 에러 처리 * fix: 신고된 리뷰 안보이도록 쿼리 수정
1 parent 3c14f4e commit 94501af

10 files changed

Lines changed: 151 additions & 10 deletions

File tree

src/main/java/umc/catchy/domain/course/api/CourseController.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public ResponseEntity<BaseResponse<PlaceVisitedResponse>> visitCheck(@PathVariab
143143

144144
return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, response));
145145
}
146-
146+
147147
@Operation(summary = "홈화면 추천 코스 API", description = "홈화면에서 사용자 맞춤 추천 코스를 조회합니다. 사용자 코스와 AI 코스를 조합하여 최대 10개를 반환합니다.")
148148
@GetMapping("/home/personal-courses")
149149
public ResponseEntity<BaseResponse<List<CourseRecommendationResponse>>> getHomeRecommendedCourses() {
@@ -157,4 +157,15 @@ public ResponseEntity<BaseResponse<List<PopularCourseInfoResponse>>> getPopularC
157157
List<PopularCourseInfoResponse> response = courseService.getPopularCourses();
158158
return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, response));
159159
}
160+
161+
@Operation(summary = "코스 리뷰 전체보기 API", description = "코스 리뷰 전체를 보여줍니다.")
162+
@GetMapping("/{courseId}/review/all")
163+
public ResponseEntity<BaseResponse<PostCourseReviewResponse.courseReviewAllResponseDTO>> searchAllReview(
164+
@PathVariable Long courseId,
165+
@RequestParam int pageSize,
166+
@RequestParam(required = false) Long lastReviewId
167+
){
168+
PostCourseReviewResponse.courseReviewAllResponseDTO response = courseReviewService.getAllCourseReview(courseId, pageSize, lastReviewId);
169+
return ResponseEntity.ok(BaseResponse.onSuccess(SuccessStatus._OK, response));
170+
}
160171
}

src/main/java/umc/catchy/domain/course/converter/CourseConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ public static List<PopularCourseInfoResponse> toPopularCourseInfoResponseList(Li
6363
.build()
6464
).collect(Collectors.toList());
6565
}
66+
67+
6668
}

src/main/java/umc/catchy/domain/courseReview/converter/CourseReviewConverter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import umc.catchy.domain.courseReview.dto.response.PostCourseReviewResponse;
77
import umc.catchy.domain.member.domain.Member;
88

9-
import java.time.LocalDateTime;
9+
import java.time.LocalDate;
1010
import java.util.List;
1111

1212
public class CourseReviewConverter {
@@ -19,6 +19,7 @@ public static PostCourseReviewResponse.newCourseReviewResponseDTO toNewCourseRev
1919
.reviewId(courseReview.getId())
2020
.comment(courseReview.getComment())
2121
.reviewImages(images)
22+
.createdAt(courseReview.getCreatedAt())
2223
.creatorNickname(courseReview.getMember().getNickname())
2324
.build();
2425
}
@@ -28,6 +29,7 @@ public static CourseReview toCourseReview(Member member, Course course, PostCour
2829
.comment(request.getComment())
2930
.member(member)
3031
.course(course)
32+
.createdAt(LocalDate.now())
3133
.build();
3234
}
3335
}

src/main/java/umc/catchy/domain/courseReview/dao/CourseReviewRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
import umc.catchy.domain.courseReview.domain.CourseReview;
77

88
@Repository
9-
public interface CourseReviewRepository extends JpaRepository<CourseReview, Long> {
9+
public interface CourseReviewRepository extends JpaRepository<CourseReview, Long>, CourseReviewRepositoryCustom {
1010
Integer countAllByCourse(Course course);
1111
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package umc.catchy.domain.courseReview.dao;
2+
3+
import org.springframework.data.domain.Pageable;
4+
import org.springframework.data.domain.Slice;
5+
import umc.catchy.domain.courseReview.dto.response.PostCourseReviewResponse;
6+
7+
public interface CourseReviewRepositoryCustom {
8+
Slice<PostCourseReviewResponse.newCourseReviewResponseDTO> getAllCourseReviewByCourseId(Long courseId,int pageSize,Long lastReviewId);
9+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package umc.catchy.domain.courseReview.dao;
2+
3+
import com.querydsl.core.types.Projections;
4+
import com.querydsl.core.types.dsl.BooleanExpression;
5+
import com.querydsl.jpa.impl.JPAQueryFactory;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.data.domain.PageRequest;
8+
import org.springframework.data.domain.Slice;
9+
import org.springframework.data.domain.SliceImpl;
10+
import umc.catchy.domain.courseReview.dto.response.PostCourseReviewResponse;
11+
12+
import java.util.List;
13+
14+
import static com.querydsl.core.group.GroupBy.groupBy;
15+
import static com.querydsl.core.group.GroupBy.list;
16+
import static umc.catchy.domain.courseReview.domain.QCourseReview.*;
17+
import static umc.catchy.domain.courseReviewImage.domain.QCourseReviewImage.*;
18+
import static umc.catchy.domain.member.domain.QMember.*;
19+
20+
@RequiredArgsConstructor
21+
public class CourseReviewRepositoryImpl implements CourseReviewRepositoryCustom{
22+
23+
private final JPAQueryFactory queryFactory;
24+
25+
@Override
26+
public Slice<PostCourseReviewResponse.newCourseReviewResponseDTO> getAllCourseReviewByCourseId(Long courseId, int pageSize, Long lastReviewId) {
27+
28+
List<Long> reviewIds = queryFactory
29+
.select(courseReview.id)
30+
.from(courseReview)
31+
.where(
32+
courseIdEq(courseId),
33+
lastCourseReviewId(lastReviewId),
34+
courseReview.isReported.eq(false)
35+
)
36+
.orderBy(courseReview.createdDate.desc())
37+
.limit(pageSize + 1)
38+
.fetch();
39+
40+
List<PostCourseReviewResponse.newCourseReviewResponseDTO> result = queryFactory.selectFrom(courseReview)
41+
.leftJoin(courseReview.member, member).on(courseReview.member.id.eq(member.id))
42+
.leftJoin(courseReviewImage).on(courseReviewImage.courseReview.id.eq(courseReview.id))
43+
.where(
44+
courseReview.id.in(reviewIds)
45+
)
46+
.orderBy(courseReview.createdDate.desc())
47+
.transform(groupBy(courseReview.id).list(
48+
Projections.fields(PostCourseReviewResponse.newCourseReviewResponseDTO.class,
49+
courseReview.id.as("reviewId"),
50+
courseReview.comment.as("comment"),
51+
list(
52+
Projections.fields(PostCourseReviewResponse.courseReviewImageResponseDTO.class,
53+
courseReviewImage.id.as("reviewImageId"),
54+
courseReviewImage.imageUrl.as("imageUrl"))
55+
).as("reviewImages"),
56+
courseReview.createdAt.as("createdAt"),
57+
courseReview.member.nickname.as("creatorNickname"))
58+
));
59+
60+
return checkLastPage(pageSize,result);
61+
}
62+
63+
private BooleanExpression courseIdEq(Long courseId) {
64+
return courseId == null ? null : courseReview.course.id.eq(courseId);
65+
}
66+
67+
private BooleanExpression lastCourseReviewId(Long lastReviewId) {
68+
if (lastReviewId == null) {
69+
return null;
70+
}
71+
return courseReview.id.lt(lastReviewId);
72+
}
73+
74+
private Slice<PostCourseReviewResponse.newCourseReviewResponseDTO> checkLastPage(int pageSize, List<PostCourseReviewResponse.newCourseReviewResponseDTO> results) {
75+
boolean hasNext = false;
76+
77+
if (results.size() > pageSize) {
78+
hasNext = true;
79+
results.remove(pageSize);
80+
}
81+
82+
return new SliceImpl<>(results, PageRequest.of(0,pageSize), hasNext);
83+
}
84+
}

src/main/java/umc/catchy/domain/courseReview/domain/CourseReview.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import umc.catchy.domain.course.domain.Course;
77
import umc.catchy.domain.member.domain.Member;
88

9+
import java.time.LocalDate;
10+
911
@Entity
1012
@Getter
1113
@Builder
@@ -17,15 +19,18 @@ public class CourseReview extends BaseTimeEntity {
1719
@Column(name = "courseReview_id")
1820
private Long id;
1921

20-
// private Integer rating;
2122

2223
private String comment;
2324

25+
private LocalDate createdAt;
26+
2427
@ManyToOne(fetch = FetchType.LAZY)
2528
@JoinColumn(name = "member_id")
2629
private Member member;
2730

2831
@ManyToOne(fetch = FetchType.LAZY)
2932
@JoinColumn(name = "course_id")
3033
private Course course;
34+
35+
private Boolean isReported = false;
3136
}

src/main/java/umc/catchy/domain/courseReview/dto/response/PostCourseReviewResponse.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import lombok.Getter;
66
import lombok.NoArgsConstructor;
77

8-
import java.time.LocalDateTime;
8+
import java.time.LocalDate;
99
import java.util.List;
1010

1111
public class PostCourseReviewResponse {
@@ -18,6 +18,7 @@ public static class newCourseReviewResponseDTO{
1818
Long reviewId;
1919
String comment;
2020
List<courseReviewImageResponseDTO> reviewImages;
21+
LocalDate createdAt;
2122
//LocalDateTime visitedDate;
2223
String creatorNickname;
2324
}
@@ -30,4 +31,15 @@ public static class courseReviewImageResponseDTO{
3031
Long reviewImageId;
3132
String imageUrl;
3233
}
34+
35+
@Getter
36+
@Builder
37+
@AllArgsConstructor
38+
@NoArgsConstructor
39+
public static class courseReviewAllResponseDTO{
40+
Double courseRating;
41+
Integer totalCount;
42+
List<PostCourseReviewResponse.newCourseReviewResponseDTO> content;
43+
Boolean last;
44+
}
3345
}

src/main/java/umc/catchy/domain/courseReview/service/CourseReviewService.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package umc.catchy.domain.courseReview.service;
22

33
import lombok.RequiredArgsConstructor;
4+
import org.springframework.data.domain.Slice;
45
import org.springframework.stereotype.Service;
56
import org.springframework.transaction.annotation.Transactional;
67
import org.springframework.web.multipart.MultipartFile;
@@ -18,13 +19,12 @@
1819
import umc.catchy.domain.mapping.memberCourse.domain.MemberCourse;
1920
import umc.catchy.domain.member.dao.MemberRepository;
2021
import umc.catchy.domain.member.domain.Member;
21-
import umc.catchy.domain.place.domain.Place;
2222
import umc.catchy.global.common.response.status.ErrorStatus;
2323
import umc.catchy.global.error.exception.GeneralException;
24+
import umc.catchy.global.error.exception.ResultEmptyListException;
2425
import umc.catchy.global.util.SecurityUtil;
2526
import umc.catchy.infra.aws.s3.AmazonS3Manager;
2627

27-
import java.time.LocalDateTime;
2828
import java.util.ArrayList;
2929
import java.util.List;
3030
import java.util.UUID;
@@ -57,8 +57,6 @@ public PostCourseReviewResponse.newCourseReviewResponseDTO postNewCourseReview(L
5757
throw new GeneralException(ErrorStatus.COURSE_REVIEW_INVALID_MEMBER);
5858
}
5959

60-
//코스 참여일자 가져오기
61-
6260
//CourseReview Entity 생성 및 저장
6361
CourseReview newCourseReview = CourseReviewConverter.toCourseReview(member, course, request);
6462
courseReviewRepository.save(newCourseReview);
@@ -81,4 +79,21 @@ public PostCourseReviewResponse.newCourseReviewResponseDTO postNewCourseReview(L
8179
}
8280
return CourseReviewConverter.toNewCourseReviewResponseDTO(newCourseReview, images);
8381
}
82+
83+
@Transactional(readOnly = true)
84+
public PostCourseReviewResponse.courseReviewAllResponseDTO getAllCourseReview(Long courseId, int pageSize, Long lastReviewId ) {
85+
Course course = courseRepository.findById(courseId).orElseThrow(() -> new GeneralException(ErrorStatus.COURSE_NOT_FOUND));
86+
Integer countReviews = courseReviewRepository.countAllByCourse(course);
87+
if (countReviews == 0) throw new ResultEmptyListException(ErrorStatus.COURSE_REVIEW_NOT_FOUND);
88+
89+
Slice<PostCourseReviewResponse.newCourseReviewResponseDTO> courseReviewResponses = courseReviewRepository.getAllCourseReviewByCourseId(courseId, pageSize, lastReviewId);
90+
List<PostCourseReviewResponse.newCourseReviewResponseDTO> content = courseReviewResponses.getContent();
91+
boolean last = courseReviewResponses.isLast();
92+
return PostCourseReviewResponse.courseReviewAllResponseDTO.builder()
93+
.courseRating(course.getRating())
94+
.totalCount(countReviews)
95+
.content(content)
96+
.last(last)
97+
.build();
98+
}
8499
}

src/main/java/umc/catchy/domain/mapping/memberCourse/domain/MemberCourse.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import umc.catchy.domain.course.domain.Course;
1010
import umc.catchy.domain.member.domain.Member;
1111

12+
import java.time.LocalDate;
1213
import java.time.LocalDateTime;
1314

1415
@Entity
@@ -25,7 +26,7 @@ public class MemberCourse extends BaseTimeEntity {
2526

2627
private boolean isVisited = false;
2728

28-
private LocalDateTime visitedDate;
29+
private LocalDate visitedDate;
2930

3031
private boolean bookmark = false;
3132

0 commit comments

Comments
 (0)