Skip to content

Commit

Permalink
Merge pull request #37 from IT-Cotato/feat/search-#35
Browse files Browse the repository at this point in the history
[Feat] 검색 기능 구현
  • Loading branch information
chanmin-00 authored Jan 28, 2025
2 parents fe95544 + 1d06886 commit 54738ad
Show file tree
Hide file tree
Showing 39 changed files with 696 additions and 127 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ dependencies {
// amazon s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// JACKSON
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'com.fasterxml.jackson.core:jackson-databind'

}

clean { delete file('src/main/generated')}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/ripple/BE/RippleApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableCaching
@EnableScheduling
@SpringBootApplication
public class RippleApplication {
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/ripple/BE/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.ripple.BE.global.config;

import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.CacheKeyPrefix;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
Expand All @@ -22,4 +27,23 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connec

return template;
}

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration configuration =
RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofMinutes(1)) // 캐시 만료 시간
.computePrefixWith(CacheKeyPrefix.simple())
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));

return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory)
.cacheDefaults(configuration)
.build();
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/ripple/BE/global/entity/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.ripple.BE.global.entity;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
Expand All @@ -15,10 +19,14 @@
AuditingEntityListener.class) // JPA 엔티티의 상태 변화를 감지하여 Auditing(자동 필드 값 설정)을 수행하는 리스너를 추가
public abstract class BaseEntity {

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@CreatedDate
@Column(updatable = false, nullable = false)
private LocalDateTime createdDate;

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime modifiedDate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ public class NewsController {
@Operation(summary = "뉴스 목록 조회", description = "뉴스 목록을 조회합니다.")
@GetMapping
public ResponseEntity<ApiResponse<Object>> getNewsList(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @RequestParam(required = false, defaultValue = "0") @PositiveOrZero int page,
final @RequestParam(required = false, defaultValue = "RECENT") NewsSort sort,
final @RequestParam(required = false) NewsCategory category) {

NewsListDTO newsListDTO = newsService.getNewsList(page, sort, category);
NewsListDTO newsListDTO = newsService.getNewsList(page, sort, category, currentUser.getId());

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(NewsListResponse.toNewsListResponse(newsListDTO)));
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/ripple/BE/news/domain/News.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Table(name = "news")
@Getter
Expand Down Expand Up @@ -57,6 +59,8 @@ public class News extends BaseEntity {
@OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> imageList = new ArrayList<>();

@Setter @Transient private Boolean isScrapped;

public static News toNewsEntity(NewsDTO newsDTO) {
return News.builder()
.title(newsDTO.title())
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/com/ripple/BE/news/dto/NewsDTO.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.ripple.BE.news.dto;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.ripple.BE.image.dto.ImageListDTO;
import com.ripple.BE.news.domain.News;
import com.ripple.BE.news.domain.type.NewsCategory;
Expand All @@ -13,7 +17,10 @@ public record NewsDTO(
String url,
Long views,
NewsCategory category,
LocalDateTime createdDate,
Boolean isScraped,
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
LocalDateTime createdDate,
ImageListDTO imageList) {

public static NewsDTO toNewsDTO(final News news) {
Expand All @@ -25,6 +32,7 @@ public static NewsDTO toNewsDTO(final News news) {
news.getUrl(),
news.getViews(),
news.getCategory(),
news.getIsScrapped(),
news.getCreatedDate(),
ImageListDTO.toImageListDTO(news.getImageList()));
}
Expand All @@ -35,6 +43,6 @@ public static NewsDTO toNewsDTO(
final String publisher,
final String url,
final NewsCategory category) {
return new NewsDTO(null, title, content, publisher, url, null, category, null, null);
return new NewsDTO(null, title, content, publisher, url, null, category, null, null, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import com.ripple.BE.news.dto.NewsListDTO;
import java.util.List;

public record NewsListResponse(List<NewsResponse> newsList, int totalPage, int currentPage) {
public record NewsListResponse(List<NewsPreviewResponse> newsList, int totalPage, int currentPage) {

public static NewsListResponse toNewsListResponse(NewsListDTO newsListDTO) {
return new NewsListResponse(
newsListDTO.newsDTOList().stream().map(NewsResponse::toNewsResponse).toList(),
newsListDTO.newsDTOList().stream().map(NewsPreviewResponse::toNewsPreviewResponse).toList(),
newsListDTO.totalPage(),
newsListDTO.currentPage());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ripple.BE.news.dto.response;

import com.ripple.BE.global.utils.RelativeTimeFormatter;
import com.ripple.BE.news.dto.NewsDTO;

public record NewsPreviewResponse(
Long id,
String title,
String content,
String publisher,
long views,
String url,
String category,
Boolean isScraped,
String createdDate) {

public static NewsPreviewResponse toNewsPreviewResponse(NewsDTO newDTO) {
return new NewsPreviewResponse(
newDTO.id(),
newDTO.title(),
newDTO.content(),
newDTO.publisher(),
newDTO.views(),
newDTO.url(),
newDTO.category().toString(),
newDTO.isScraped(),
RelativeTimeFormatter.formatRelativeTime(newDTO.createdDate()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

public interface NewsRepositoryCustom {

Page<News> findByCategory(NewsCategory category, NewsSort newsSort, Pageable pageable);
Page<News> findByCategory(
NewsCategory category, NewsSort newsSort, Pageable pageable, long userId);

Page<News> findAll(Pageable pageable, NewsSort newsSort);
Page<News> findAll(Pageable pageable, NewsSort newsSort, long userId);

Page<News> searchNews(String keyword, Pageable pageable, long userId);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.ripple.BE.news.repository.news;

import static com.ripple.BE.news.domain.QNews.*;
import static com.ripple.BE.news.domain.QNewsScrap.*;

import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.ripple.BE.news.domain.News;
import com.ripple.BE.news.domain.QNews;
import com.ripple.BE.news.domain.type.NewsCategory;
import com.ripple.BE.news.domain.type.NewsSort;
import java.util.List;
Expand All @@ -20,47 +23,73 @@ public class NewsRepositoryCustomImpl implements NewsRepositoryCustom {
private final JPAQueryFactory queryFactory;

@Override
public Page<News> findByCategory(NewsCategory category, NewsSort newsSort, Pageable pageable) {
public Page<News> findByCategory(
NewsCategory category, NewsSort newsSort, Pageable pageable, long userId) {
BooleanExpression predicate = news.category.eq(category);

List<News> newsList = getNewsByPageable(pageable, predicate, newsSort);
List<News> newsList = getNewsWithScrapByPageable(pageable, predicate, newsSort, userId);

JPAQuery<Long> countQuery = queryFactory.select(news.count()).from(news).where(predicate);

return PageableExecutionUtils.getPage(newsList, pageable, countQuery::fetchOne);
}

@Override
public Page<News> findAll(Pageable pageable, NewsSort newsSort) {
public Page<News> searchNews(String keyword, Pageable pageable, long userId) {
BooleanExpression predicate = null;

List<News> newsList = getNewsByPageable(pageable, null, newsSort);
if (keyword != null && !keyword.trim().isEmpty()) {
predicate = news.title.contains(keyword).or(news.content.contains(keyword));
}

List<News> newsList = getNewsWithScrapByPageable(pageable, predicate, NewsSort.RECENT, userId);

JPAQuery<Long> countQuery = queryFactory.select(news.count()).from(news).where(predicate);

return PageableExecutionUtils.getPage(newsList, pageable, countQuery::fetchOne);
}

@Override
public Page<News> findAll(Pageable pageable, NewsSort newsSort, long userId) {

List<News> newsList = getNewsWithScrapByPageable(pageable, null, newsSort, userId);

JPAQuery<Long> countQuery = queryFactory.select(news.count()).from(news);

return PageableExecutionUtils.getPage(newsList, pageable, countQuery::fetchOne);
}

private List<News> getNewsByPageable(
Pageable pageable, BooleanExpression predicate, NewsSort newsSort) {
if (newsSort == NewsSort.POPULAR) {
return queryFactory
.selectFrom(news)
.where(predicate)
.orderBy(
news.views.desc(), // 조회수 내림차순
news.createdDate.desc() // 생성일 내림차순
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
} else {
return queryFactory
.selectFrom(news)
.where(predicate)
.orderBy(news.createdDate.desc()) // 생성일 내림차순
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
private List<News> getNewsWithScrapByPageable(
Pageable pageable, BooleanExpression predicate, NewsSort newsSort, long userId) {

// 동적으로 정렬 조건 설정
var orderBy =
(newsSort == NewsSort.POPULAR)
? new com.querydsl.core.types.OrderSpecifier[] {
news.views.desc(), news.createdDate.desc()
}
: new com.querydsl.core.types.OrderSpecifier[] {news.createdDate.desc()};

List<Tuple> results =
queryFactory
.select(news, newsScrap.id)
.from(news)
.leftJoin(newsScrap)
.on(news.id.eq(newsScrap.news.id).and(newsScrap.user.id.eq(userId)))
.where(predicate)
.orderBy(orderBy)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

return results.stream()
.map(
tuple -> {
News news = tuple.get(QNews.news);
Long scrapId = tuple.get(newsScrap.id);
news.setIsScrapped(scrapId != null);
return news;
})
.toList();
}
}
12 changes: 9 additions & 3 deletions src/main/java/com/ripple/BE/news/service/NewsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -39,16 +40,21 @@ public class NewsService {

private static final int PAGE_SIZE = 10;

@Cacheable(
value = "newsList",
key =
"#page + (#sort != null ? #sort.toString() : '') + (#category != null ? #category.toString() : '')")
@Transactional(readOnly = true)
public NewsListDTO getNewsList(final int page, final NewsSort sort, final NewsCategory category) {
public NewsListDTO getNewsList(
final int page, final NewsSort sort, final NewsCategory category, final long userId) {

Pageable pageable = PageRequest.of(page, PAGE_SIZE);

// 게시글 조회 (타입에 따른 필터링)
Page<News> newsPage =
category == null
? newsRepository.findAll(pageable, sort) // 일반 게시글 조회
: newsRepository.findByCategory(category, sort, pageable); // 특정 타입 게시글 조회
? newsRepository.findAll(pageable, sort, userId) // 일반 게시글 조회
: newsRepository.findByCategory(category, sort, pageable, userId); // 특정 타입 게시글 조회

return NewsListDTO.toNewsListDTO(newsPage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ public ResponseEntity<ApiResponse<Object>> deletePost(
@Operation(summary = "게시글 목록 조회", description = "게시글 목록을 조회합니다.")
@GetMapping
public ResponseEntity<ApiResponse<Object>> getPosts(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @RequestParam(required = false, defaultValue = "0") @PositiveOrZero int page,
final @RequestParam(required = false, defaultValue = "RECENT") PostSort sort,
final @RequestParam(required = false) PostType type) {

PostListDTO postListDTO = postService.getPosts(page, sort, type);
PostListDTO postListDTO = postService.getPosts(page, sort, type, currentUser.getId());

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(PostListResponse.toPostListResponse(postListDTO)));
Expand Down
Loading

0 comments on commit 54738ad

Please sign in to comment.