11package com .nova .nova_server .domain .cardNews .repository ;
22
3+ import com .nova .nova_server .domain .cardNews .dto .CardNewsIdScoreResult ;
4+ import com .nova .nova_server .domain .cardNews .dto .CardNewsScoreResult ;
35import com .nova .nova_server .domain .cardNews .dto .CardNewsSearchCondition ;
6+ import com .nova .nova_server .domain .cardNews .dto .QCardNewsIdScoreResult ;
47import com .nova .nova_server .domain .cardNews .entity .CardNews ;
58import com .nova .nova_server .domain .cardNews .entity .CardType ;
9+ import com .nova .nova_server .domain .feed .enums .FeedSort ;
10+ import com .nova .nova_server .global .config .FeedConfig ;
11+ import com .querydsl .core .types .OrderSpecifier ;
612import com .querydsl .core .types .dsl .BooleanExpression ;
13+ import com .querydsl .core .types .dsl .Expressions ;
14+ import com .querydsl .core .types .dsl .NumberExpression ;
715import com .querydsl .jpa .JPAExpressions ;
816import com .querydsl .jpa .impl .JPAQuery ;
917import com .querydsl .jpa .impl .JPAQueryFactory ;
1422import org .springframework .stereotype .Repository ;
1523
1624import java .time .LocalDateTime ;
25+ import java .util .ArrayList ;
1726import java .util .List ;
27+ import java .util .Map ;
28+ import java .util .stream .Collectors ;
1829
1930import static com .nova .nova_server .domain .cardNews .entity .QCardNews .cardNews ;
2031import static com .nova .nova_server .domain .cardNews .entity .QCardNewsBookmark .cardNewsBookmark ;
2132import static com .nova .nova_server .domain .cardNews .entity .QCardNewsKeyword .cardNewsKeyword ;
2233import static com .nova .nova_server .domain .keyword .entity .QKeyword .keyword ;
34+ import static com .nova .nova_server .domain .member .entity .QMemberPreferKeyword .memberPreferKeyword ;
2335
2436@ Slf4j
2537@ Repository
@@ -28,12 +40,20 @@ public class CardNewsRepositoryImpl implements CardNewsRepositoryCustom {
2840
2941 private final JPAQueryFactory queryFactory ;
3042
43+ private final FeedConfig feedConfig ;
44+
3145 @ Override
32- public Page <CardNews > searchByCondition (CardNewsSearchCondition condition ) {
46+ public Page <CardNewsScoreResult > searchByCondition (CardNewsSearchCondition condition ) {
3347
34- // 필터링 및 정렬 수행
35- List <Long > idList = queryFactory
36- .select (cardNews .id )
48+ // 검색 조건에 맞는 ID와 관련도 점수 조회
49+ List <CardNewsIdScoreResult > cardNewsIdScoreList = queryFactory
50+ .select (
51+ new QCardNewsIdScoreResult (
52+ cardNews .id ,
53+ // 최신순 정렬 시에도 관련도 점수는 표시되어야 함
54+ calcTotalScore (condition .memberId ())
55+ )
56+ )
3757 .from (cardNews )
3858 .where (
3959 cardTypeIn (condition .type ()),
@@ -42,12 +62,12 @@ public Page<CardNews> searchByCondition(CardNewsSearchCondition condition) {
4262 publishedBefore (condition .endDate ()),
4363 isSaved (condition .memberId (), condition .saved ())
4464 )
45- .orderBy (cardNews . publishedAt . desc ()) // TODO: 관련도순 정렬 구현 예정
65+ .orderBy (getOrderSpecifiers ( condition . memberId (), condition . sort ()))
4666 .offset (condition .pageable ().getOffset ())
4767 .limit (condition .pageable ().getPageSize ())
4868 .fetch ();
4969
50- // 전체 개수 조회
70+ // 검색 조건에 맞는 전체 개수 조회
5171 JPAQuery <Long > countQuery = queryFactory
5272 .select (cardNews .count ())
5373 .from (cardNews )
@@ -59,9 +79,9 @@ public Page<CardNews> searchByCondition(CardNewsSearchCondition condition) {
5979 isSaved (condition .memberId (), condition .saved ())
6080 );
6181
62- log .debug ("Fetched CardNews ids {} by condition {}" , idList , condition );
82+ log .debug ("Fetched CardNews ids {} by condition {}" , cardNewsIdScoreList , condition );
6383
64- if (idList .isEmpty ()) {
84+ if (cardNewsIdScoreList .isEmpty ()) {
6585 return Page .empty (condition .pageable ());
6686 }
6787
@@ -71,10 +91,21 @@ public Page<CardNews> searchByCondition(CardNewsSearchCondition condition) {
7191 .from (cardNews )
7292 .leftJoin (cardNews .keywords , cardNewsKeyword ).fetchJoin ()
7393 .leftJoin (cardNewsKeyword .keyword , keyword ).fetchJoin ()
74- .where (cardIdIn (idList ))
94+ .where (cardIdIn (cardNewsIdScoreList . stream (). map ( CardNewsIdScoreResult :: cardNewsId ). toList () ))
7595 .fetch ();
7696
77- return PageableExecutionUtils .getPage (cardNewsList , condition .pageable (), countQuery ::fetchOne );
97+ // 카드 뉴스별 점수 매핑
98+ Map <Long , CardNews > cardNewsMap = cardNewsList .stream ()
99+ .collect (Collectors .toMap (CardNews ::getId , cardNews -> cardNews ));
100+
101+ List <CardNewsScoreResult > cardNewsScoreList = cardNewsIdScoreList .stream ()
102+ .map (result -> CardNewsScoreResult .builder ()
103+ .cardNews (cardNewsMap .get (result .cardNewsId ()))
104+ .score (result .score ())
105+ .build ())
106+ .toList ();
107+
108+ return PageableExecutionUtils .getPage (cardNewsScoreList , condition .pageable (), countQuery ::fetchOne );
78109 }
79110
80111 private BooleanExpression cardIdIn (List <Long > idList ) {
@@ -100,7 +131,7 @@ private BooleanExpression keywordIn(List<String> keywords) {
100131
101132 return cardNews .id .in (
102133 JPAExpressions
103- .select (cardNewsKeyword .cardNewsId ).distinct ()
134+ .select (cardNewsKeyword .cardNews . id ).distinct ()
104135 .from (cardNewsKeyword )
105136 .join (cardNewsKeyword .keyword , keyword )
106137 .where (keyword .name .in (keywords ))
@@ -130,9 +161,9 @@ private BooleanExpression isSaved(Long memberId, Boolean saved) {
130161
131162 return cardNews .id .in (
132163 JPAExpressions
133- .select (cardNewsBookmark .cardNewsId )
164+ .select (cardNewsBookmark .cardNews . id )
134165 .from (cardNewsBookmark )
135- .where (cardNewsBookmark .memberId .eq (memberId ))
166+ .where (cardNewsBookmark .member . id .eq (memberId ))
136167 );
137168 }
138169
@@ -183,4 +214,85 @@ private BooleanExpression containsKeyword(String keyword) {
183214 .or (cardNews .summary .contains (keyword ));
184215 }
185216
217+ /**
218+ * 정렬 기준에 따른 OrderSpecifier 배열 반환
219+ */
220+ private OrderSpecifier <?>[] getOrderSpecifiers (Long memberId , FeedSort sort ) {
221+ if (sort == null ) {
222+ sort = FeedSort .LATEST ;
223+ }
224+
225+ List <OrderSpecifier <?>> orderSpecifiers = new ArrayList <>();
226+ if (sort == FeedSort .RELEVANCE ) {
227+ orderSpecifiers .add (calcTotalScore (memberId ).desc ());
228+ }
229+ orderSpecifiers .add (cardNews .publishedAt .desc ());
230+
231+ return orderSpecifiers .toArray (new OrderSpecifier <?>[0 ]);
232+ }
233+
234+ /**
235+ * 최종 관련도 점수 계산
236+ */
237+ private NumberExpression <Integer > calcTotalScore (Long memberId ) {
238+ if (memberId == null ) {
239+ return Expressions .asNumber (feedConfig .getBaseScore ());
240+ }
241+
242+ return Expressions
243+ .asNumber (feedConfig .getBaseScore ())
244+ .add (calcKeywordScore (memberId ))
245+ .add (calcBookmarkScore (memberId ));
246+ }
247+
248+ /**
249+ * 키워드 기반 관련도 점수 계산<br/>
250+ * 사용자의 관심 키워드와 컨텐츠 키워드가 일치하는 개수에 가중치를 곱하여 점수 부여
251+ */
252+ private NumberExpression <Integer > calcKeywordScore (long memberId ) {
253+ return Expressions .numberTemplate (
254+ Integer .class ,
255+ "({0} * {1})" ,
256+ JPAExpressions
257+ .select (cardNewsKeyword .count ())
258+ .from (cardNewsKeyword )
259+ .where (
260+ cardNewsKeyword .cardNews .id .eq (cardNews .id ),
261+ cardNewsKeyword .keyword .id .in (
262+ JPAExpressions
263+ .select (memberPreferKeyword .keyword .id )
264+ .from (memberPreferKeyword )
265+ .where (memberPreferKeyword .member .id .eq (memberId ))
266+ )
267+ ),
268+ feedConfig .getKeywordMatchScore ()
269+ );
270+ }
271+
272+ /**
273+ * 북마크 기반 관련도 점수 계산<br/>
274+ * 사용자가 북마크한 카드뉴스와 키워드가 일치하는 개수에 가중치를 곱하여 점수 부여
275+ */
276+ private NumberExpression <Integer > calcBookmarkScore (long memberId ) {
277+ return Expressions .numberTemplate (
278+ Integer .class ,
279+ "({0} * {1})" ,
280+ JPAExpressions
281+ .select (cardNewsKeyword .count ())
282+ .from (cardNewsKeyword )
283+ .where (
284+ cardNewsKeyword .cardNews .id .eq (cardNews .id ),
285+ cardNewsKeyword .keyword .id .in (
286+ JPAExpressions
287+ .select (cardNewsKeyword .keyword .id )
288+ .from (cardNewsKeyword )
289+ .leftJoin (cardNewsBookmark )
290+ .on (cardNewsBookmark .cardNews .id .eq (cardNewsKeyword .cardNews .id ))
291+ .where (cardNewsBookmark .member .id .eq (memberId ))
292+ )
293+ ),
294+ feedConfig .getBookmarkKeywordMatchScore ()
295+ );
296+ }
297+
186298}
0 commit comments