Skip to content
11 changes: 11 additions & 0 deletions back/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import org.gradle.kotlin.dsl.annotationProcessor
import org.gradle.kotlin.dsl.testAnnotationProcessor

plugins {
java
id("org.springframework.boot") version "3.5.5"
Expand Down Expand Up @@ -43,6 +46,14 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")


// QueryDSL
implementation("io.github.openfeign.querydsl:querydsl-jpa:7.0")
annotationProcessor("io.github.openfeign.querydsl:querydsl-apt:7.0:jpa")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
testAnnotationProcessor("io.github.openfeign.querydsl:querydsl-apt:7.0:jpa")
testAnnotationProcessor("jakarta.persistence:jakarta.persistence-api")
}

tasks.withType<Test> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.back.domain.post.dto.PostRequest;
import com.back.domain.post.dto.PostResponse;
import com.back.domain.post.dto.PostSearchCondition;
import com.back.domain.post.service.PostService;
import com.back.global.common.ApiResponse;
import com.back.global.common.PageResponse;
Expand Down Expand Up @@ -36,8 +37,9 @@ public ApiResponse<PostResponse> createPost(

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public record PostResponse(
Long id,
String title,
String content,
String author,
PostCategory category,
boolean hide,
int likeCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.back.domain.post.dto;

import com.back.domain.post.enums.PostCategory;
import com.back.domain.post.enums.SearchType;

/**
* 검색 조건
* @param category (CHAT, SCENARIO, POLL)
* @param searchType (TITLE, TITLE_CONTENT, AUTHOR)
* @param keyword (검색어)
*/
public record PostSearchCondition(
PostCategory category,
SearchType searchType,
String keyword
) {
}
11 changes: 11 additions & 0 deletions back/src/main/java/com/back/domain/post/enums/SearchType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.back.domain.post.enums;

/**
* 검색 타입
* TITLE - 제목
* TITLE_CONTENT - 제목 + 내용
* AUTHOR - 작성자
*/
public enum SearchType {
TITLE, TITLE_CONTENT, AUTHOR
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static PostResponse toResponse(Post post) {
post.getId(),
post.getTitle(),
post.getContent(),
post.getUser().getNickname(),
post.getCategory(),
post.isHide(),
post.getLikeCount(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
* 게시글 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
*/
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.back.domain.post.repository;

import com.back.domain.post.dto.PostSearchCondition;
import com.back.domain.post.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface PostRepositoryCustom {
Page<Post> searchPosts(PostSearchCondition postSearchCondition, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.back.domain.post.repository;

import com.back.domain.post.dto.PostSearchCondition;
import com.back.domain.post.entity.Post;
import com.back.domain.post.enums.PostCategory;
import com.back.domain.post.enums.SearchType;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

import java.util.List;

import static com.back.domain.post.entity.QPost.post;
import static com.back.domain.user.entity.QUser.user;

@RequiredArgsConstructor
@Repository
public class PostRepositoryCustomImpl implements PostRepositoryCustom {
private final JPAQueryFactory queryFactory;

@Override
public Page<Post> searchPosts(PostSearchCondition condition, Pageable pageable) {
List<Post> posts = queryFactory
.selectFrom(post)
.leftJoin(post.user, user).fetchJoin()
.where(getCategoryCondition(condition.category()),
getSearchCondition(condition.keyword(), condition.searchType()))
.orderBy(post.createdDate.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

JPAQuery<Long> count = queryFactory
.select(post.count())
.from(post)
.where(
getCategoryCondition(condition.category()),
getSearchCondition(condition.keyword(), condition.searchType())
);

return PageableExecutionUtils.getPage(posts, pageable, count::fetchOne);
}

/**
* 1차 필터링 (CHAT, SCENARIO, POLL)
* category 조건이 null이 아니면 필터링 조건 추가
*/
private BooleanExpression getCategoryCondition(PostCategory category) {
return category != null ? post.category.eq(category) : null;
}

/**
* 2차 필터링 (TITLE, TITLE_CONTENT, AUTHOR)
* fixme 현재 like 기반 검색 - 성능 최적화를 위해 추후에 수정 예정
*/
private BooleanExpression getSearchCondition(String searchKeyword, SearchType searchType) {
if (!StringUtils.hasText(searchKeyword) || searchType == null) {
return null;
}

return switch (searchType) {
case TITLE -> post.title.containsIgnoreCase(searchKeyword);
case TITLE_CONTENT -> post.title.containsIgnoreCase(searchKeyword)
.or(post.content.containsIgnoreCase(searchKeyword));
case AUTHOR -> post.user.nickname.containsIgnoreCase(searchKeyword);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.back.domain.post.dto.PostRequest;
import com.back.domain.post.dto.PostResponse;
import com.back.domain.post.dto.PostSearchCondition;
import com.back.domain.post.entity.Post;
import com.back.domain.post.mapper.PostMapper;
import com.back.domain.post.repository.PostRepository;
Expand Down Expand Up @@ -48,8 +49,8 @@ public PostResponse getPost(Long postId) {
.orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND));
}

public Page<PostResponse> getPosts(Pageable pageable) {
return postRepository.findAll(pageable)
public Page<PostResponse> getPosts(PostSearchCondition condition, Pageable pageable) {
return postRepository.searchPosts(condition, pageable)
.map(PostMapper::toResponse);
}

Expand Down
14 changes: 14 additions & 0 deletions back/src/main/java/com/back/global/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.back.global.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JpaConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
Loading