Skip to content

Commit da722b1

Browse files
authored
refactor: 페이징 관련 구조 통합
* feat: 컨트롤러 limit, page 요청 통일 * Feat: 쿼리 대문자로 변경 * refactor: 쿠폰 관련 필요없는 주석 제거 * feat: 레슨 관련 페이징 통합 및 구조 정리 * refactor: 쿼리 대소문자 수정 * feat: 프로필 관련 페이징 구조 통일 * feat: 리뷰 관련 n+1 문제 해결 -> profile 쪽 같이 join 통해 * feat: 레슨 관련 N + 1 문제 해결
1 parent 3f2bcf3 commit da722b1

File tree

18 files changed

+543
-334
lines changed

18 files changed

+543
-334
lines changed

src/main/java/com/threestar/trainus/domain/comment/controller/CommentController.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22

33
import java.util.List;
44

5-
import org.springframework.beans.factory.annotation.Value;
65
import org.springframework.http.HttpStatus;
76
import org.springframework.http.ResponseEntity;
87
import org.springframework.web.bind.annotation.DeleteMapping;
98
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.ModelAttribute;
1010
import org.springframework.web.bind.annotation.PathVariable;
1111
import org.springframework.web.bind.annotation.PostMapping;
1212
import org.springframework.web.bind.annotation.RequestBody;
1313
import org.springframework.web.bind.annotation.RequestMapping;
14-
import org.springframework.web.bind.annotation.RequestParam;
1514
import org.springframework.web.bind.annotation.RestController;
1615

1716
import com.threestar.trainus.domain.comment.dto.CommentCreateRequestDto;
1817
import com.threestar.trainus.domain.comment.dto.CommentPageResponseDto;
1918
import com.threestar.trainus.domain.comment.dto.CommentResponseDto;
2019
import com.threestar.trainus.domain.comment.service.CommentService;
2120
import com.threestar.trainus.global.annotation.LoginUser;
21+
import com.threestar.trainus.global.dto.PageRequestDto;
2222
import com.threestar.trainus.global.unit.BaseResponse;
2323
import com.threestar.trainus.global.unit.PagedResponse;
2424

@@ -34,8 +34,6 @@
3434
public class CommentController {
3535

3636
private final CommentService commentService;
37-
@Value("${spring.page.size.limit}")
38-
private int pageSizeLimit;
3937

4038
@PostMapping("/{lessonId}")
4139
@Operation(summary = "댓글 작성", description = "레슨 ID에 해당되는 댓글을 작성합니다.")
@@ -48,11 +46,9 @@ public ResponseEntity<BaseResponse<CommentResponseDto>> createComment(@PathVaria
4846
@GetMapping("/{lessonId}")
4947
@Operation(summary = "댓글 조회", description = "레슨 ID에 해당되는 댓글들을 조회합니다.")
5048
public ResponseEntity<PagedResponse<List<CommentResponseDto>>> readAll(@PathVariable Long lessonId,
51-
@RequestParam("page") int page,
52-
@RequestParam("pageSize") int pageSize) {
53-
int correctPage = Math.max(page, 1);
54-
int correctPageSize = Math.max(1, Math.min(pageSize, pageSizeLimit));
55-
CommentPageResponseDto commentsInfo = commentService.readAll(lessonId, correctPage, correctPageSize);
49+
@Valid @ModelAttribute PageRequestDto pageRequestDto) {
50+
CommentPageResponseDto commentsInfo = commentService.readAll(lessonId, pageRequestDto.getPage(),
51+
pageRequestDto.getLimit());
5652
return PagedResponse.ok("댓글 조회 성공", commentsInfo.comments(), commentsInfo.count(), HttpStatus.OK);
5753
}
5854

src/main/java/com/threestar/trainus/domain/comment/repository/CommentRepository.java

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,46 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
1414

1515
//create index idx_lesson_id_parent_comment_id_comment_id on comments(lesson_id, parent_comment_id asc, comment_id asc); 인덱스 통해 조회 성능 최적화
1616
@Query(value = """
17-
select
18-
c.comment_id as commentId,
19-
c.content as content,
20-
c.parent_comment_id as parentCommentId,
21-
c.deleted as deleted,
22-
c.created_at as createdAt,
23-
u.id as userId,
24-
u.nickname as nickname
25-
from (
26-
select comment_id
27-
from comments c
28-
join user u on c.user_id = u.id and u.deleted_at IS NULL
29-
where lesson_id = :lessonId
30-
order by parent_comment_id asc, comment_id asc
31-
limit :limit offset :offset
17+
SELECT
18+
c.comment_id AS commentId,
19+
c.content AS content,
20+
c.parent_comment_id AS parentCommentId,
21+
c.deleted AS deleted,
22+
c.created_at AS createdAt,
23+
u.id AS userId,
24+
u.nickname AS nickname
25+
FROM (
26+
SELECT comment_id
27+
FROM comments c
28+
JOIN user u ON c.user_id = u.id AND u.deleted_at IS NULL
29+
WHERE lesson_id = :lessonId
30+
ORDER BY parent_comment_id ASC, comment_id ASC
31+
LIMIT :limit OFFSET :offset
3232
) t
33-
join comments c on t.comment_id = c.comment_id
34-
join user u on c.user_id = u.id
33+
JOIN comments c ON t.comment_id = c.comment_id
34+
JOIN user u ON c.user_id = u.id
3535
""", nativeQuery = true)
3636
List<CommentWithUserProjection> findAll(@Param("lessonId") Long lessonId, @Param("offset") int offset,
3737
@Param("limit") int limit);
3838

3939
@Query(value = """
40-
select count(*) from (
41-
select comment_id
42-
from comments c
43-
join user u on c.user_id = u.id and u.deleted_at IS NULL
44-
where lesson_id = :lessonId
45-
limit :limit
40+
SELECT count(*) FROM (
41+
SELECT comment_id
42+
FROM comments c
43+
JOIN user u ON c.user_id = u.id AND u.deleted_at IS NULL
44+
WHERE lesson_id = :lessonId
45+
LIMIT :limit
4646
) t
4747
""", nativeQuery = true)
4848
Integer count(@Param("lessonId") Long lessonId, @Param("limit") int limit);
4949

5050
@Query(value = """
51-
select count(*) from (
52-
select comment_id
53-
from comments c
54-
join user u on c.user_id = u.id and u.deleted_at IS NULL
55-
where lesson_id = :lessonId and parent_comment_id = :parentCommentId
56-
limit :limit
51+
SELECT count(*) FROM (
52+
SELECT comment_id
53+
FROM comments c
54+
JOIN user u ON c.user_id = u.id AND u.deleted_at IS NULL
55+
WHERE lesson_id = :lessonId AND parent_comment_id = :parentCommentId
56+
LIMIT :limit
5757
) t
5858
""", nativeQuery = true)
5959
Long countBy(@Param("lessonId") Long lessonId, @Param("parentCommentId") Long parentCommentId,

src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import java.util.List;
44

5-
import org.springframework.data.domain.Page;
6-
75
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto;
86
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto;
97
import com.threestar.trainus.domain.coupon.admin.dto.CouponDeleteResponseDto;
@@ -59,15 +57,6 @@ public static CouponListItemDto toCouponListItemDto(Coupon coupon) {
5957
);
6058
}
6159

62-
// public static CouponListResponseDto toCouponListResponseDto(Page<Coupon> couponPage) {
63-
// return new CouponListResponseDto(
64-
// (int)couponPage.getTotalElements(),
65-
// couponPage.getContent().stream()
66-
// .map(AdminCouponMapper::toCouponListItemDto)
67-
// .toList()
68-
// );
69-
// }
70-
7160
public static CouponListResponseDto toCouponListResponseDto(
7261
List<Coupon> coupons, int totalCount
7362
) {
@@ -79,7 +68,6 @@ public static CouponListResponseDto toCouponListResponseDto(
7968
);
8069
}
8170

82-
8371
public static CouponDetailResponseDto toCouponDetailResponseDto(Coupon coupon, Integer issuedCount) {
8472
return new CouponDetailResponseDto(
8573
coupon.getId(),

src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33
import java.time.LocalDateTime;
44
import java.util.List;
55

6-
import org.springframework.data.domain.Page;
7-
import org.springframework.data.domain.PageRequest;
8-
import org.springframework.data.domain.Pageable;
9-
import org.springframework.data.domain.Sort;
106
import org.springframework.stereotype.Service;
117
import org.springframework.transaction.annotation.Transactional;
128

@@ -55,20 +51,6 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto request, Long
5551
}
5652

5753
//쿠폰 조회
58-
// @Transactional(readOnly = true)
59-
// public CouponListResponseDto getCoupons(int page, int limit, CouponStatus status, CouponCategory category,
60-
// Long userId) {
61-
// // 관리자 권한 검증
62-
// userService.validateAdminRole(userId);
63-
//
64-
// Pageable pageable = PageRequest.of(page - 1, limit, Sort.by("createdAt").descending());
65-
//
66-
// // 조건에 따른 쿠폰 조회
67-
// Page<Coupon> couponPage = couponRepository.findCouponsWithFilters(status, category, pageable);
68-
//
69-
// //응답 DTO 변환
70-
// return AdminCouponMapper.toCouponListResponseDto(couponPage);
71-
// }
7254
@Transactional(readOnly = true)
7355
public CouponListResponseDto getCoupons(
7456
int page, int limit,

src/main/java/com/threestar/trainus/domain/lesson/student/service/StudentLessonService.java

Lines changed: 61 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package com.threestar.trainus.domain.lesson.student.service;
22

3+
import java.util.Collections;
34
import java.util.List;
45
import java.util.Optional;
56

6-
import org.springframework.data.domain.Page;
7-
import org.springframework.data.domain.PageRequest;
8-
import org.springframework.data.domain.Pageable;
9-
import org.springframework.data.domain.Sort;
107
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
119

1210
import com.threestar.trainus.domain.lesson.student.dto.LessonApplicationResponseDto;
1311
import com.threestar.trainus.domain.lesson.student.dto.LessonDetailResponseDto;
@@ -42,8 +40,8 @@
4240
import com.threestar.trainus.domain.user.service.UserService;
4341
import com.threestar.trainus.global.exception.domain.ErrorCode;
4442
import com.threestar.trainus.global.exception.handler.BusinessException;
43+
import com.threestar.trainus.global.utils.PageLimitCalculator;
4544

46-
import jakarta.transaction.Transactional;
4745
import lombok.RequiredArgsConstructor;
4846

4947
@Service
@@ -59,68 +57,51 @@ public class StudentLessonService {
5957
private final LessonParticipantRepository lessonParticipantRepository;
6058
private final LessonApplicationRepository lessonApplicationRepository;
6159

62-
@Transactional
60+
@Transactional(readOnly = true)
6361
public LessonSearchListResponseDto searchLessons(
64-
int page, int limit,
62+
int page, int pageSize,
6563
Category category, String search,
6664
String city, String district, String dong, String ri,
6765
LessonSortType sortBy
6866
) {
69-
// 정렬 조건 처리
70-
Sort sort = Sort.unsorted();
71-
if (sortBy != null) {
72-
switch (sortBy) {
73-
case LATEST:
74-
sort = Sort.by(Sort.Direction.DESC, sortBy.getProperty());
75-
break;
76-
case OLDEST:
77-
sort = Sort.by(Sort.Direction.ASC, sortBy.getProperty());
78-
break;
79-
case PRICE_HIGH:
80-
sort = Sort.by(Sort.Direction.DESC, sortBy.getProperty());
81-
break;
82-
case PRICE_LOW:
83-
sort = Sort.by(Sort.Direction.ASC, sortBy.getProperty());
84-
break;
85-
}
86-
} else {
67+
if (sortBy == null) {
8768
throw new BusinessException(ErrorCode.INVALID_SORT);
8869
}
8970

90-
Pageable pageable = PageRequest.of(page - 1, limit, sort);
71+
// offset/limit 계산
72+
int offset = (page - 1) * pageSize;
73+
int countLimit = PageLimitCalculator.calculatePageLimit(page, pageSize, 5);
74+
75+
// 카테고리 ALL 처리
76+
String categoryValue = (category != null && !category.name().equalsIgnoreCase("ALL"))
77+
? category.name() : null;
78+
79+
// Lesson 목록 조회 (검색어 여부에 따라 분기)
80+
List<Lesson> lessons = (search != null && !search.isEmpty())
81+
? lessonRepository.findLessonsWithFullText(
82+
categoryValue, city, district, dong, ri, search, sortBy.name(), offset, pageSize
83+
)
84+
: lessonRepository.findLessonsWithoutFullText(
85+
categoryValue, city, district, dong, ri, sortBy.name(), offset, pageSize
86+
);
9187

92-
Category categoryEnum = null;
93-
if (category != null && !category.name().equalsIgnoreCase("ALL")) {
94-
categoryEnum = category;
95-
}
88+
// count 조회 (검색어 여부에 따라 분기)
89+
int total = (search != null && !search.isEmpty())
90+
? lessonRepository.countLessonsWithFullText(
91+
categoryValue, city, district, dong, ri, search, countLimit
92+
)
93+
: lessonRepository.countLessonsWithoutFullText(
94+
categoryValue, city, district, dong, ri, countLimit
95+
);
9696

97-
Page<Lesson> lessonPage;
98-
// 검색어 유무 분기
99-
if (search != null && !search.isEmpty()) {
100-
lessonPage = lessonRepository.findByLocationAndFullTextSearchOptimized(
101-
categoryEnum, city, district, dong, ri, search, pageable
102-
);
103-
} else {
104-
lessonPage = lessonRepository.findByLocation(
105-
categoryEnum, city, district, dong, ri, pageable
106-
);
107-
}
108-
// 응답 DTO 리스트 매핑
109-
List<LessonSearchResponseDto> lessonDtos = lessonPage.getContent().stream()
97+
// DTO 매핑
98+
List<LessonSearchResponseDto> lessonDtos = lessons.stream()
11099
.map(lesson -> {
111-
// 개설자 정보 조회
112100
User leader = userService.getUserById(lesson.getLessonLeader());
113-
114-
// 프로필 이미지
115101
Profile profile = profileRepository.findByUserId(leader.getId())
116102
.orElseThrow(() -> new BusinessException(ErrorCode.PROFILE_NOT_FOUND));
117-
/*
118-
* TODO: 프로필 공통예외처리 분리
119-
* */
120-
// 리뷰 개수, 평점 등 메타데이터
121103
ProfileMetadataResponseDto metadata = profileMetadataService.getMetadata(leader.getId());
122104

123-
// 이미지 URL 목록
124105
List<String> imageUrls = lessonImageRepository.findAllByLessonId(lesson.getId()).stream()
125106
.map(LessonImage::getImageUrl)
126107
.toList();
@@ -129,7 +110,7 @@ public LessonSearchListResponseDto searchLessons(
129110
})
130111
.toList();
131112

132-
return new LessonSearchListResponseDto(lessonDtos, (int)lessonPage.getTotalElements());
113+
return new LessonSearchListResponseDto(lessonDtos, total);
133114
}
134115

135116
@Transactional
@@ -298,33 +279,46 @@ public void cancelLessonApplication(Long lessonId, Long userId) {
298279
lessonApplicationRepository.delete(application);
299280
}
300281

301-
@Transactional
302-
public MyLessonApplicationListResponseDto getMyLessonApplications(Long userId, int page, int limit,
303-
String statusStr) {
282+
@Transactional(readOnly = true)
283+
public MyLessonApplicationListResponseDto getMyLessonApplications(
284+
Long userId, int page, int limit, String statusStr
285+
) {
304286
// status enum 변환
305287
ApplicationStatus status = null;
306-
if (!statusStr.equalsIgnoreCase("ALL")) {
307-
//status value 검증
288+
if (!"ALL".equalsIgnoreCase(statusStr)) {
308289
try {
309290
status = ApplicationStatus.valueOf(statusStr.toUpperCase());
310291
} catch (IllegalArgumentException e) {
311292
throw new BusinessException(ErrorCode.INVALID_APPLICATION_STATUS);
312293
}
313294
}
314295

315-
// 페이징 정렬
316-
Pageable pageable = PageRequest.of(page - 1, limit);
296+
// offset 계산
297+
int offset = (page - 1) * limit;
317298

318-
// 신청 내역 조회
319-
Page<LessonApplication> applicationPage = (status == null)
320-
? lessonApplicationRepository.findByUserId(userId, pageable)
321-
: lessonApplicationRepository.findByUserIdAndStatus(userId, status, pageable);
299+
// count limit 계산 (5개씩 이동 기준)
300+
int countLimit = PageLimitCalculator.calculatePageLimit(page, limit, 5);
322301

323-
// DTO 변환
324-
return LessonApplicationMapper.toDtoListWithCount(
325-
applicationPage.getContent(),
326-
(int)applicationPage.getTotalElements()
302+
// 목록 조회
303+
List<Long> ids = lessonApplicationRepository.findIdsByUserAndStatus(
304+
userId,
305+
status != null ? status.name() : null,
306+
offset,
307+
limit
327308
);
309+
310+
List<LessonApplication> applications = ids.isEmpty()
311+
? Collections.emptyList()
312+
: lessonApplicationRepository.findAllWithFetchJoin(ids);
313+
314+
// count 조회 (최대 countLimit까지만 계산)
315+
int total = lessonApplicationRepository.countByUserAndStatus(
316+
userId,
317+
status != null ? status.name() : null,
318+
countLimit
319+
);
320+
321+
return LessonApplicationMapper.toDtoListWithCount(applications, total);
328322
}
329323

330324
@Transactional

0 commit comments

Comments
 (0)