Skip to content

Commit d2bf731

Browse files
authored
[FEAT]: 게시글 검색 기능 추가 (#18)
* [Chore]: QueryDSL 의존성 추가 * [Chore]: QueryDSL 사용을 위한 QueryFactory 빈 추가 * [Chore]: 검색 타입 enum 추가 (제목, 제목+내용, 작성자) * [Chore]: 검색 시 필요한 요청 DTO 추가 * [Feat]: 게시글 검색 기능 구현 * [Feat]: 응답 DTO 게시글 작성자 추가 * [Feat]: 검색 기능 구현에 따른 파라미터, 메서드 호출 수정 * [Test]: 목록 조회 테스트 수정 및 테스트 데이터 Fixture 클래스로 분리
1 parent b57add6 commit d2bf731

File tree

13 files changed

+338
-106
lines changed

13 files changed

+338
-106
lines changed

back/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import org.gradle.kotlin.dsl.annotationProcessor
2+
import org.gradle.kotlin.dsl.testAnnotationProcessor
3+
14
plugins {
25
java
36
id("org.springframework.boot") version "3.5.5"
@@ -43,6 +46,14 @@ dependencies {
4346
testImplementation("org.springframework.boot:spring-boot-starter-test")
4447
testImplementation("org.springframework.security:spring-security-test")
4548
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
49+
50+
51+
// QueryDSL
52+
implementation("io.github.openfeign.querydsl:querydsl-jpa:7.0")
53+
annotationProcessor("io.github.openfeign.querydsl:querydsl-apt:7.0:jpa")
54+
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
55+
testAnnotationProcessor("io.github.openfeign.querydsl:querydsl-apt:7.0:jpa")
56+
testAnnotationProcessor("jakarta.persistence:jakarta.persistence-api")
4657
}
4758

4859
tasks.withType<Test> {

back/src/main/java/com/back/domain/post/controller/PostController.java

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

33
import com.back.domain.post.dto.PostRequest;
44
import com.back.domain.post.dto.PostResponse;
5+
import com.back.domain.post.dto.PostSearchCondition;
56
import com.back.domain.post.service.PostService;
67
import com.back.global.common.ApiResponse;
78
import com.back.global.common.PageResponse;
@@ -36,8 +37,9 @@ public ApiResponse<PostResponse> createPost(
3637

3738
// 게시글 목록 조회
3839
@GetMapping
39-
public ApiResponse<PageResponse<PostResponse>> getPosts(Pageable pageable) {
40-
Page<PostResponse> responses = postService.getPosts(pageable);
40+
public ApiResponse<PageResponse<PostResponse>> getPosts(
41+
@ModelAttribute PostSearchCondition condition, Pageable pageable) {
42+
Page<PostResponse> responses = postService.getPosts(condition, pageable);
4143
return ApiResponse.success(PageResponse.of(responses), "성공적으로 조회되었습니다.", HttpStatus.OK);
4244
}
4345

back/src/main/java/com/back/domain/post/dto/PostResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public record PostResponse(
1818
Long id,
1919
String title,
2020
String content,
21+
String author,
2122
PostCategory category,
2223
boolean hide,
2324
int likeCount,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.back.domain.post.dto;
2+
3+
import com.back.domain.post.enums.PostCategory;
4+
import com.back.domain.post.enums.SearchType;
5+
6+
/**
7+
* 검색 조건
8+
* @param category (CHAT, SCENARIO, POLL)
9+
* @param searchType (TITLE, TITLE_CONTENT, AUTHOR)
10+
* @param keyword (검색어)
11+
*/
12+
public record PostSearchCondition(
13+
PostCategory category,
14+
SearchType searchType,
15+
String keyword
16+
) {
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.back.domain.post.enums;
2+
3+
/**
4+
* 검색 타입
5+
* TITLE - 제목
6+
* TITLE_CONTENT - 제목 + 내용
7+
* AUTHOR - 작성자
8+
*/
9+
public enum SearchType {
10+
TITLE, TITLE_CONTENT, AUTHOR
11+
}

back/src/main/java/com/back/domain/post/mapper/PostMapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static PostResponse toResponse(Post post) {
2626
post.getId(),
2727
post.getTitle(),
2828
post.getContent(),
29+
post.getUser().getNickname(),
2930
post.getCategory(),
3031
post.isHide(),
3132
post.getLikeCount(),

back/src/main/java/com/back/domain/post/repository/PostRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
* 게시글 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
99
*/
1010
@Repository
11-
public interface PostRepository extends JpaRepository<Post, Long> {
11+
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
1212
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.back.domain.post.repository;
2+
3+
import com.back.domain.post.dto.PostSearchCondition;
4+
import com.back.domain.post.entity.Post;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
8+
public interface PostRepositoryCustom {
9+
Page<Post> searchPosts(PostSearchCondition postSearchCondition, Pageable pageable);
10+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.back.domain.post.repository;
2+
3+
import com.back.domain.post.dto.PostSearchCondition;
4+
import com.back.domain.post.entity.Post;
5+
import com.back.domain.post.enums.PostCategory;
6+
import com.back.domain.post.enums.SearchType;
7+
import com.querydsl.core.types.dsl.BooleanExpression;
8+
import com.querydsl.jpa.impl.JPAQuery;
9+
import com.querydsl.jpa.impl.JPAQueryFactory;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.data.support.PageableExecutionUtils;
14+
import org.springframework.stereotype.Repository;
15+
import org.springframework.util.StringUtils;
16+
17+
import java.util.List;
18+
19+
import static com.back.domain.post.entity.QPost.post;
20+
import static com.back.domain.user.entity.QUser.user;
21+
22+
@RequiredArgsConstructor
23+
@Repository
24+
public class PostRepositoryCustomImpl implements PostRepositoryCustom {
25+
private final JPAQueryFactory queryFactory;
26+
27+
@Override
28+
public Page<Post> searchPosts(PostSearchCondition condition, Pageable pageable) {
29+
List<Post> posts = queryFactory
30+
.selectFrom(post)
31+
.leftJoin(post.user, user).fetchJoin()
32+
.where(getCategoryCondition(condition.category()),
33+
getSearchCondition(condition.keyword(), condition.searchType()))
34+
.orderBy(post.createdDate.desc())
35+
.offset(pageable.getOffset())
36+
.limit(pageable.getPageSize())
37+
.fetch();
38+
39+
JPAQuery<Long> count = queryFactory
40+
.select(post.count())
41+
.from(post)
42+
.where(
43+
getCategoryCondition(condition.category()),
44+
getSearchCondition(condition.keyword(), condition.searchType())
45+
);
46+
47+
return PageableExecutionUtils.getPage(posts, pageable, count::fetchOne);
48+
}
49+
50+
/**
51+
* 1차 필터링 (CHAT, SCENARIO, POLL)
52+
* category 조건이 null이 아니면 필터링 조건 추가
53+
*/
54+
private BooleanExpression getCategoryCondition(PostCategory category) {
55+
return category != null ? post.category.eq(category) : null;
56+
}
57+
58+
/**
59+
* 2차 필터링 (TITLE, TITLE_CONTENT, AUTHOR)
60+
* fixme 현재 like 기반 검색 - 성능 최적화를 위해 추후에 수정 예정
61+
*/
62+
private BooleanExpression getSearchCondition(String searchKeyword, SearchType searchType) {
63+
if (!StringUtils.hasText(searchKeyword) || searchType == null) {
64+
return null;
65+
}
66+
67+
return switch (searchType) {
68+
case TITLE -> post.title.containsIgnoreCase(searchKeyword);
69+
case TITLE_CONTENT -> post.title.containsIgnoreCase(searchKeyword)
70+
.or(post.content.containsIgnoreCase(searchKeyword));
71+
case AUTHOR -> post.user.nickname.containsIgnoreCase(searchKeyword);
72+
};
73+
}
74+
}

back/src/main/java/com/back/domain/post/service/PostService.java

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

33
import com.back.domain.post.dto.PostRequest;
44
import com.back.domain.post.dto.PostResponse;
5+
import com.back.domain.post.dto.PostSearchCondition;
56
import com.back.domain.post.entity.Post;
67
import com.back.domain.post.mapper.PostMapper;
78
import com.back.domain.post.repository.PostRepository;
@@ -48,8 +49,8 @@ public PostResponse getPost(Long postId) {
4849
.orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND));
4950
}
5051

51-
public Page<PostResponse> getPosts(Pageable pageable) {
52-
return postRepository.findAll(pageable)
52+
public Page<PostResponse> getPosts(PostSearchCondition condition, Pageable pageable) {
53+
return postRepository.searchPosts(condition, pageable)
5354
.map(PostMapper::toResponse);
5455
}
5556

0 commit comments

Comments
 (0)