|
6 | 6 | import com.bootsignal.domain.course.dto.PriceRange; |
7 | 7 | import com.bootsignal.domain.course.dto.FieldCategory; |
8 | 8 | import com.bootsignal.domain.course_session.entity.CourseSession; |
| 9 | +import com.bootsignal.domain.crawled_review.entity.CrawledReview; |
| 10 | +import com.bootsignal.domain.review.entity.Review; |
9 | 11 | import jakarta.persistence.criteria.JoinType; |
| 12 | +import jakarta.persistence.criteria.Order; |
10 | 13 | import jakarta.persistence.criteria.Path; |
11 | 14 | import jakarta.persistence.criteria.Root; |
12 | 15 | import jakarta.persistence.criteria.Subquery; |
13 | 16 | import org.springframework.data.jpa.domain.Specification; |
14 | 17 | import org.springframework.util.StringUtils; |
15 | 18 |
|
16 | 19 | import java.time.LocalDate; |
| 20 | +import java.util.ArrayList; |
| 21 | +import java.util.List; |
17 | 22 | import java.util.Set; |
18 | 23 |
|
19 | 24 | /** |
@@ -162,6 +167,110 @@ public static Specification<CourseSession> orderByBookmarkCountDesc() { |
162 | 167 | }; |
163 | 168 | } |
164 | 169 |
|
| 170 | + /** |
| 171 | + * 최신순 Criteria 정렬 — prioritizeFull과 함께 사용할 때 Pageable Sort 대신 적용합니다. |
| 172 | + */ |
| 173 | + public static Specification<CourseSession> orderByLatest() { |
| 174 | + return (root, query, cb) -> { |
| 175 | + if (query != null && Long.class != query.getResultType() && long.class != query.getResultType()) { |
| 176 | + query.orderBy(cb.desc(root.get("id"))); |
| 177 | + } |
| 178 | + return cb.conjunction(); |
| 179 | + }; |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * 만족도순 Criteria 정렬 — prioritizeFull과 함께 사용할 때 Pageable Sort 대신 적용합니다. |
| 184 | + */ |
| 185 | + public static Specification<CourseSession> orderBySatisfaction() { |
| 186 | + return (root, query, cb) -> { |
| 187 | + if (query != null && Long.class != query.getResultType() && long.class != query.getResultType()) { |
| 188 | + var courseJoin = root.join("course", JoinType.LEFT); |
| 189 | + query.orderBy( |
| 190 | + cb.desc(courseJoin.get("stdgScor")), |
| 191 | + cb.desc(root.get("id")) |
| 192 | + ); |
| 193 | + } |
| 194 | + return cb.conjunction(); |
| 195 | + }; |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * 취업률순 Criteria 정렬 — prioritizeFull과 함께 사용할 때 Pageable Sort 대신 적용합니다. |
| 200 | + */ |
| 201 | + public static Specification<CourseSession> orderByEmploymentRate() { |
| 202 | + return (root, query, cb) -> { |
| 203 | + if (query != null && Long.class != query.getResultType() && long.class != query.getResultType()) { |
| 204 | + query.orderBy( |
| 205 | + cb.desc(root.get("employmentRate")), |
| 206 | + cb.desc(root.get("id")) |
| 207 | + ); |
| 208 | + } |
| 209 | + return cb.conjunction(); |
| 210 | + }; |
| 211 | + } |
| 212 | + |
| 213 | + /** |
| 214 | + * 기관 이미지·만족도·취업률·별점이 모두 있는 과정을 기존 정렬 기준 내에서 우선 노출합니다. |
| 215 | + * 기존 ORDER BY 앞에 완전성 우선순위(0=완전, 1=불완전)를 삽입합니다. |
| 216 | + * 반드시 기본 정렬 Specification을 체이닝한 뒤 마지막에 추가해야 합니다. |
| 217 | + */ |
| 218 | + public static Specification<CourseSession> orderByCompleteDataFirst() { |
| 219 | + return (root, query, cb) -> { |
| 220 | + if (query == null || Long.class == query.getResultType() || long.class == query.getResultType()) { |
| 221 | + return cb.conjunction(); |
| 222 | + } |
| 223 | + |
| 224 | + var courseJoin = root.join("course", JoinType.LEFT); |
| 225 | + var institutionJoin = courseJoin.join("institution", JoinType.LEFT); |
| 226 | + |
| 227 | + // 사용자 리뷰 존재 여부 서브쿼리 |
| 228 | + Subquery<Long> reviewCount = query.subquery(Long.class); |
| 229 | + Root<Review> review = reviewCount.from(Review.class); |
| 230 | + reviewCount.select(cb.count(review.get("id"))); |
| 231 | + reviewCount.where( |
| 232 | + cb.and( |
| 233 | + cb.equal(review.get("course").get("id"), courseJoin.get("id")), |
| 234 | + cb.isNull(review.get("deletedAt")) |
| 235 | + ) |
| 236 | + ); |
| 237 | + |
| 238 | + // 크롤링 리뷰 존재 여부 서브쿼리 |
| 239 | + Subquery<Long> crawledReviewCount = query.subquery(Long.class); |
| 240 | + Root<CrawledReview> crawled = crawledReviewCount.from(CrawledReview.class); |
| 241 | + crawledReviewCount.select(cb.count(crawled.get("id"))); |
| 242 | + crawledReviewCount.where( |
| 243 | + cb.and( |
| 244 | + cb.equal(crawled.get("course").get("id"), courseJoin.get("id")), |
| 245 | + cb.isNotNull(crawled.get("rating")) |
| 246 | + ) |
| 247 | + ); |
| 248 | + |
| 249 | + // 4가지 조건 모두 충족 시 우선순위 0, 아닐 경우 1 |
| 250 | + var completePriority = cb.selectCase() |
| 251 | + .when( |
| 252 | + cb.and( |
| 253 | + cb.isNotNull(institutionJoin.get("profileImageUrl")), |
| 254 | + cb.isNotNull(courseJoin.get("stdgScor")), |
| 255 | + cb.isNotNull(root.get("employmentRate")), |
| 256 | + cb.or( |
| 257 | + cb.greaterThan(reviewCount, 0L), |
| 258 | + cb.greaterThan(crawledReviewCount, 0L) |
| 259 | + ) |
| 260 | + ), |
| 261 | + 0 |
| 262 | + ) |
| 263 | + .otherwise(1); |
| 264 | + |
| 265 | + List<Order> orders = new ArrayList<>(); |
| 266 | + orders.add(cb.asc(completePriority)); |
| 267 | + orders.addAll(query.getOrderList()); |
| 268 | + query.orderBy(orders); |
| 269 | + |
| 270 | + return cb.conjunction(); |
| 271 | + }; |
| 272 | + } |
| 273 | + |
165 | 274 | /** |
166 | 275 | * 모집 마감 임박순 정렬입니다. |
167 | 276 | * 시작 예정 과정은 시작일 오름차순으로 먼저 노출하고, 이미 시작했거나 시작일이 없는 과정은 뒤로 보냅니다. |
|
0 commit comments