Skip to content

Commit 501e2b4

Browse files
authored
AIM-79-챌린지-및-게시글-통합-검색 (#75)
* 홈 화면 검색을 위한 게시글 및 챌린지 통합 검색 API 구현 * feat: 게시글+챌린지 통합 검색 API 추가
1 parent 5976538 commit 501e2b4

File tree

10 files changed

+396
-44
lines changed

10 files changed

+396
-44
lines changed

src/main/java/targeter/aim/domain/challenge/controller/ChallengeReadController.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,18 @@ public ChallengeDto.ChallengePageResponse getLikedChallenges(
9999
return challengeService.getLikedChallenges(request, userDetails, pageable);
100100
}
101101

102-
}
102+
@NoJwtAuth
103+
@GetMapping("/search")
104+
@Operation(
105+
summary = "챌린지 검색",
106+
description = "키워드 기반으로 공개(PUBLIC) + (로그인 시) 내가 참여한 PRIVATE 챌린지까지 검색합니다."
107+
)
108+
public ChallengeDto.ChallengePageResponse searchChallenges(
109+
@RequestParam(required = false) String keyword,
110+
@RequestParam(defaultValue = "LATEST") ChallengeDto.ChallengeSortType sort,
111+
@PageableDefault(size = 16) @ParameterObject Pageable pageable,
112+
@AuthenticationPrincipal UserDetails userDetails
113+
) {
114+
return challengeService.searchChallenges(keyword, sort, pageable, userDetails);
115+
}
116+
}

src/main/java/targeter/aim/domain/challenge/repository/ChallengeQueryRepository.java

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import targeter.aim.domain.challenge.entity.ChallengeMode;
1717
import targeter.aim.domain.challenge.entity.ChallengeStatus;
1818
import targeter.aim.domain.challenge.entity.QChallenge;
19+
import targeter.aim.domain.challenge.entity.ChallengeVisibility;
1920
import targeter.aim.domain.file.dto.FileDto;
2021
import targeter.aim.domain.file.entity.ChallengeImage;
2122
import targeter.aim.domain.file.entity.ProfileImage;
@@ -44,9 +45,9 @@ public class ChallengeQueryRepository {
4445

4546
private final JPAQueryFactory queryFactory;
4647

47-
/**
48-
* 1. VS 챌린지용 Query
49-
*/
48+
/**
49+
* 1. VS 챌린지용 Query
50+
*/
5051

5152
// 기존 전체 조회용
5253
public Page<ChallengeDto.ChallengeListResponse> paginateVsByType(
@@ -105,6 +106,7 @@ public Page<ChallengeDto.ChallengeListResponse> paginateVsByTypeAndKeywordAndFie
105106

106107
return slice(sorted, pageable);
107108
}
109+
108110
JPAQuery<Tuple> query = buildVsBaseQuery(userDetails, filterType, keyword, field);
109111
applyVsSorting(query, sortType);
110112

@@ -602,6 +604,114 @@ public List<Challenge> findSimpleMyChallenges(Long userId, ChallengeMode mode) {
602604
.fetch(); // List로 반환
603605
}
604606

607+
/**
608+
* 4. 전체(공개 + 내가 참여한 비공개) 검색용 Query
609+
*/
610+
public Page<ChallengeDto.ChallengeListResponse> paginateSearchAll(
611+
UserDetails userDetails,
612+
Pageable pageable,
613+
ChallengeDto.ChallengeSortType sortType
614+
) {
615+
return paginateSearchAllByKeyword(userDetails, pageable, sortType, null);
616+
}
617+
618+
public Page<ChallengeDto.ChallengeListResponse> paginateSearchAllByKeyword(
619+
UserDetails userDetails,
620+
Pageable pageable,
621+
ChallengeDto.ChallengeSortType sortType,
622+
String keyword
623+
) {
624+
JPAQuery<Tuple> query = buildPublicAllBaseQuery(userDetails, keyword);
625+
applySorting(query, sortType);
626+
627+
List<Tuple> tuples = query
628+
.offset(pageable.getOffset())
629+
.limit(pageable.getPageSize())
630+
.fetch();
631+
632+
Long total = buildCountPublicAllQuery(userDetails, keyword).fetchOne();
633+
634+
return new PageImpl<>(
635+
enrichDetails(tuples),
636+
pageable,
637+
total == null ? 0 : total
638+
);
639+
}
640+
641+
private JPAQuery<Tuple> buildPublicAllBaseQuery(
642+
UserDetails userDetails,
643+
String keyword
644+
) {
645+
JPAQuery<Tuple> query = queryFactory
646+
.select(
647+
challenge,
648+
userDetails != null
649+
? JPAExpressions.selectOne()
650+
.from(challengeLiked)
651+
.where(
652+
challengeLiked.challenge.eq(challenge)
653+
.and(challengeLiked.user.id.eq(userDetails.getUser().getId()))
654+
).exists()
655+
: Expressions.FALSE,
656+
JPAExpressions.select(challengeLiked.count())
657+
.from(challengeLiked)
658+
.where(challengeLiked.challenge.eq(challenge))
659+
)
660+
.from(challenge)
661+
.leftJoin(challenge.host).fetchJoin()
662+
.leftJoin(challenge.host.tier).fetchJoin()
663+
.leftJoin(challenge.host.profileImage).fetchJoin()
664+
.where(visibleToUser(userDetails));
665+
666+
BooleanExpression keywordPredicate = keywordCondition(keyword);
667+
if (keywordPredicate != null) {
668+
query.where(keywordPredicate);
669+
}
670+
671+
return query;
672+
}
673+
674+
private JPAQuery<Long> buildCountPublicAllQuery(
675+
UserDetails userDetails,
676+
String keyword
677+
) {
678+
JPAQuery<Long> query = queryFactory
679+
.select(challenge.count())
680+
.from(challenge)
681+
.where(visibleToUser(userDetails));
682+
683+
BooleanExpression keywordPredicate = keywordCondition(keyword);
684+
if (keywordPredicate != null) {
685+
query.where(keywordPredicate);
686+
}
687+
688+
return query;
689+
}
690+
691+
private BooleanExpression visibleToUser(UserDetails userDetails) {
692+
if (userDetails == null) {
693+
return challenge.visibility.eq(ChallengeVisibility.PUBLIC);
694+
}
695+
696+
BooleanExpression isPublic = challenge.visibility.eq(ChallengeVisibility.PUBLIC);
697+
698+
BooleanExpression isMyPrivate = challenge.visibility.eq(ChallengeVisibility.PRIVATE)
699+
.and(
700+
JPAExpressions.selectOne()
701+
.from(challengeMember)
702+
.where(
703+
challengeMember.id.challenge.eq(challenge),
704+
challengeMember.id.user.id.eq(userDetails.getUser().getId())
705+
)
706+
.exists()
707+
);
708+
709+
return isPublic.or(isMyPrivate);
710+
}
711+
712+
/**
713+
* 5. 내가 좋아요 누른 챌린지 목록 조회용 Query
714+
*/
605715
public Page<ChallengeDto.ChallengeListResponse> paginateLiked(
606716
UserDetails userDetails,
607717
Pageable pageable,
@@ -616,6 +726,11 @@ public Page<ChallengeDto.ChallengeListResponse> paginateLikedByKeyword(
616726
ChallengeDto.ChallengeSortType sortType,
617727
String keyword
618728
) {
729+
// liked는 로그인 필수
730+
if (userDetails == null) {
731+
return new PageImpl<>(List.of(), pageable, 0);
732+
}
733+
619734
JPAQuery<Tuple> query = buildLikedBaseQuery(userDetails, keyword);
620735
applySorting(query, sortType);
621736

@@ -677,5 +792,4 @@ private JPAQuery<Long> buildCountLikedQuery(
677792

678793
return query;
679794
}
680-
681795
}

src/main/java/targeter/aim/domain/challenge/service/ChallengeReadService.java

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public ChallengeDto.ChallengePageResponse getVsChallenges(
4747

4848
Page<ChallengeDto.ChallengeListResponse> page;
4949

50-
if(field != null) {
50+
if (field != null) {
5151
page = challengeQueryRepository.paginateVsByTypeAndKeywordAndField(
5252
userDetails, pageable, filterType, sortType, keyword, field
5353
);
@@ -76,10 +76,10 @@ private String normalizeField(String field) {
7676
if (field == null) return null;
7777

7878
String f = field.trim();
79-
if(f.isEmpty()) return null;
79+
if (f.isEmpty()) return null;
8080

8181
String upper = f.toUpperCase(Locale.ROOT);
82-
if("ALL".equals(upper)) return null;
82+
if ("ALL".equals(upper)) return null;
8383

8484
return upper;
8585
}
@@ -127,7 +127,7 @@ public ChallengeDto.VsChallengeOverviewResponse getVsChallengeOverview(
127127
User me = hostUser;
128128
User opponent = memberUser;
129129

130-
if(loginUserId != null && memberUser != null && loginUserId.equals(memberUser.getId())) {
130+
if (loginUserId != null && memberUser != null && loginUserId.equals(memberUser.getId())) {
131131
me = memberUser;
132132
opponent = hostUser;
133133
}
@@ -201,7 +201,7 @@ public ChallengeDto.VsChallengeOverviewResponse getVsChallengeOverview(
201201
);
202202

203203
boolean isLiked = false;
204-
if(loginUserId != null) {
204+
if (loginUserId != null) {
205205
isLiked = challengeLikedRepository.existsByUserAndChallenge(userDetails.getUser(), challenge);
206206
}
207207

@@ -312,7 +312,7 @@ public ChallengeDto.SoloChallengeOverviewResponse getSoloChallengeOverview(
312312
);
313313

314314
boolean isLiked = false;
315-
if(loginUserId != null) {
315+
if (loginUserId != null) {
316316
isLiked = challengeLikedRepository.existsByUserAndChallenge(userDetails.getUser(), challenge);
317317
}
318318

@@ -349,15 +349,55 @@ public ChallengeDto.ChallengePageResponse getAllChallenges(
349349
return ChallengeDto.ChallengePageResponse.from(page);
350350
}
351351

352+
// 홈 화면(또는 검색 화면)에서 쓰는 "공개 + (로그인 시) 내가 참여한 PRIVATE" 통합 검색
353+
public ChallengeDto.ChallengePageResponse searchAllChallenges(
354+
ChallengeDto.AllListSearchCondition condition,
355+
UserDetails userDetails,
356+
Pageable pageable
357+
) {
358+
ChallengeDto.ChallengeSortType sortType = condition.getSort();
359+
String keyword = normalizeKeyword(condition.getKeyword());
360+
361+
Page<ChallengeDto.ChallengeListResponse> page =
362+
keyword != null
363+
? challengeQueryRepository.paginateSearchAllByKeyword(
364+
userDetails, pageable, sortType, keyword
365+
)
366+
: challengeQueryRepository.paginateSearchAll(
367+
userDetails, pageable, sortType
368+
);
369+
370+
return ChallengeDto.ChallengePageResponse.from(page);
371+
}
372+
373+
public ChallengeDto.ChallengePageResponse searchChallenges(
374+
String keyword,
375+
ChallengeDto.ChallengeSortType sortType,
376+
Pageable pageable,
377+
UserDetails userDetails
378+
) {
379+
String k = normalizeKeyword(keyword);
380+
381+
Page<ChallengeDto.ChallengeListResponse> page =
382+
k != null
383+
? challengeQueryRepository.paginateSearchAllByKeyword(
384+
userDetails, pageable, sortType, k
385+
)
386+
: challengeQueryRepository.paginateSearchAll(
387+
userDetails, pageable, sortType
388+
);
389+
390+
return ChallengeDto.ChallengePageResponse.from(page);
391+
}
392+
393+
// /api/challenges/liked 용 (로그인 필수)
352394
public ChallengeDto.ChallengePageResponse getLikedChallenges(
353395
ChallengeDto.AllListSearchCondition condition,
354396
UserDetails userDetails,
355397
Pageable pageable
356398
) {
357399
if (userDetails == null) {
358-
return ChallengeDto.ChallengePageResponse.from(
359-
Page.empty(pageable)
360-
);
400+
return ChallengeDto.ChallengePageResponse.from(Page.empty(pageable));
361401
}
362402

363403
ChallengeDto.ChallengeSortType sortType = condition.getSort();
@@ -374,5 +414,4 @@ public ChallengeDto.ChallengePageResponse getLikedChallenges(
374414

375415
return ChallengeDto.ChallengePageResponse.from(page);
376416
}
377-
378-
}
417+
}

src/main/java/targeter/aim/domain/post/controller/PostReadController.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import io.swagger.v3.oas.annotations.Operation;
44
import io.swagger.v3.oas.annotations.Parameter;
5-
import io.swagger.v3.oas.annotations.media.Schema;
65
import io.swagger.v3.oas.annotations.tags.Tag;
76
import lombok.RequiredArgsConstructor;
87
import org.springdoc.core.annotations.ParameterObject;
@@ -52,7 +51,6 @@ public PostDto.PostVsDetailResponse getVsPostDetail(
5251
return postService.getVsPostDetail(postId, userDetails);
5352
}
5453

55-
5654
@NoJwtAuth
5755
@GetMapping("/qna")
5856
@Operation(
@@ -189,4 +187,18 @@ public PostDto.PostPageResponse getMyLikedPosts(
189187
);
190188
}
191189

190+
@NoJwtAuth
191+
@GetMapping("/search")
192+
@Operation(
193+
summary = "게시글 검색",
194+
description = "키워드 기반으로 게시글을 검색합니다."
195+
)
196+
public PostDto.PostPageResponse searchPosts(
197+
@RequestParam(required = false) String keyword,
198+
@RequestParam(defaultValue = "LATEST") PostDto.PostSortType sort,
199+
@PageableDefault(size = 16) @ParameterObject Pageable pageable,
200+
@AuthenticationPrincipal UserDetails userDetails
201+
) {
202+
return postService.searchPosts(keyword, sort, pageable, userDetails);
203+
}
192204
}

0 commit comments

Comments
 (0)