11package com .threestar .trainus .domain .lesson .student .service ;
22
3+ import java .util .Collections ;
34import java .util .List ;
45import 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 ;
107import org .springframework .stereotype .Service ;
8+ import org .springframework .transaction .annotation .Transactional ;
119
1210import com .threestar .trainus .domain .lesson .student .dto .LessonApplicationResponseDto ;
1311import com .threestar .trainus .domain .lesson .student .dto .LessonDetailResponseDto ;
4240import com .threestar .trainus .domain .user .service .UserService ;
4341import com .threestar .trainus .global .exception .domain .ErrorCode ;
4442import com .threestar .trainus .global .exception .handler .BusinessException ;
43+ import com .threestar .trainus .global .utils .PageLimitCalculator ;
4544
46- import jakarta .transaction .Transactional ;
4745import 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