Skip to content

Commit

Permalink
feat: 페스티벌 검색 관련 api 구현 (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
punchdrunkard authored Aug 17, 2024
1 parent eb21e5e commit 443599a
Show file tree
Hide file tree
Showing 21 changed files with 460 additions and 81 deletions.
2 changes: 1 addition & 1 deletion Odiga-Submodule
4 changes: 4 additions & 0 deletions src/main/java/com/odiga/fiesta/FiestaApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAspectJAutoProxy
@EnableCaching
@EnableAsync
public class FiestaApplication {

public static void main(String[] args) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/odiga/fiesta/common/PageResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class PageResponse<T> {
private final List<T> content;
private final long offset;
private final int pageSize;
private final int pageNumber;
private final long totalElements;
private final int totalPages;

Expand All @@ -21,10 +22,22 @@ public PageResponse(Page<T> page) {
this.content = page.getContent();
this.offset = page.getPageable().getOffset();
this.pageSize = page.getPageable().getPageSize();
this.pageNumber = page.getPageable().getPageNumber();
this.totalElements = page.getTotalElements();
this.totalPages = page.getTotalPages();
}

@Builder
public PageResponse(List<T> content, long offset, int pageSize, int pageNumber, long totalElements,
int totalPages) {
this.content = content;
this.offset = offset;
this.pageSize = pageSize;
this.pageNumber = pageNumber;
this.totalElements = totalElements;
this.totalPages = totalPages;
}

public static <T> PageResponse<T> of(Page<T> page) {
return new PageResponse<>(page);
}
Expand Down
23 changes: 21 additions & 2 deletions src/main/java/com/odiga/fiesta/common/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.connection.lettuce.LettuceConnectionFactory;
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;

import lombok.RequiredArgsConstructor;
Expand All @@ -26,8 +30,8 @@ public RedisConnectionFactory redisConnectionFactory() {
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
public <T> RedisTemplate<String, T> redisTemplate() {
RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());

// 일반적인 key:value의 경우 시리얼라이저
Expand All @@ -43,4 +47,19 @@ public RedisTemplate<String, Object> redisTemplate() {

return redisTemplate;
}

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/odiga/fiesta/common/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public enum ErrorCode {
FESTIVAL_CATEGORY_NOT_FOUND(400, "F004", "존재하지 않는 페스티벌 카테고리 입니다."),
INVALID_CURRENT_LOCATION(400, "F005", "현재 위치 값을 알 수 없습니다."),
FESTIVAL_NOT_FOUND(400, "F006", "페스티벌의 정보를 찾을 수 없습니다."),

QUERY_CANNOT_BE_EMPTY(400, "F007", "검색어는 필수입니다."),
QUERY_CANNOT_BE_BLANK(400, "F008", "공백으로는 검색할 수 없습니다."),
// LOG
LOG_NOT_FOUND(404, "L001", "존재하지 않는 방문일지입니다.");

Expand Down
32 changes: 27 additions & 5 deletions src/main/java/com/odiga/fiesta/common/util/RedisUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import static java.util.concurrent.TimeUnit.*;

import java.util.Set;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
Expand All @@ -11,19 +14,38 @@

@Component
@RequiredArgsConstructor
public class RedisUtils {
public class RedisUtils<T> {

private final RedisTemplate<String, Object> redisTemplate;
private final RedisTemplate<String, T> redisTemplate;

public void setData(String key, String value, Long expiredTime) {
public void setData(String key, T value, Long expiredTime) {
redisTemplate.opsForValue().set(key, value, expiredTime, MILLISECONDS);
}

public String getData(String key) {
return (String)redisTemplate.opsForValue().get(key);
public T getData(String key, Class<T> clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}

public void deleteData(String key) {
redisTemplate.delete(key);
}

// 실시간 검색어 관련
public Double zScore(String key, T member) {
return redisTemplate.opsForZSet().score(key, member);
}

public void zAdd(String key, T target, Double score) {
redisTemplate.opsForZSet().add(key, target, score);
}

public Set<ZSetOperations.TypedTuple<T>> zRevrange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}

public Long zSize(String key){
return redisTemplate.opsForZSet().size(key);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import static java.util.Objects.*;

import java.time.YearMonth;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -22,14 +24,12 @@
import com.odiga.fiesta.common.BasicResponse;
import com.odiga.fiesta.common.PageResponse;
import com.odiga.fiesta.common.error.exception.CustomException;
import com.odiga.fiesta.festival.dto.response.FestivalThisWeekResponse;
import com.odiga.fiesta.user.domain.User;
import com.odiga.fiesta.festival.dto.response.FestivalMonthlyResponse;
import com.odiga.fiesta.festival.dto.response.FestivalInfoResponse;
import com.odiga.fiesta.festival.dto.request.FestivalFilterRequest;
import com.odiga.fiesta.festival.dto.response.FestivalBasic;
import com.odiga.fiesta.festival.dto.response.FestivalBookmarkResponse;
import com.odiga.fiesta.festival.dto.response.FestivalInfoResponse;
import com.odiga.fiesta.festival.dto.response.FestivalInfo;
import com.odiga.fiesta.festival.dto.response.FestivalMonthlyResponse;
import com.odiga.fiesta.festival.dto.response.FestivalThisWeekResponse;
import com.odiga.fiesta.festival.service.FestivalBookmarkService;
import com.odiga.fiesta.festival.service.FestivalService;
import com.odiga.fiesta.user.domain.User;
Expand All @@ -49,6 +49,8 @@
@Slf4j
public class FestivalController {

private static final String KEYWORD_RANKING_KEY = "festival rank";

private final FestivalService festivalService;
private final FestivalBookmarkService festivalBookmarkService;

Expand All @@ -71,7 +73,7 @@ public ResponseEntity<BasicResponse<FestivalMonthlyResponse>> getMonthlyFestival
description = "해당 날짜의 페스티벌을 조회합니다."
)
@GetMapping("/daily")
public ResponseEntity<BasicResponse<PageResponse<FestivalInfoResponse>>> getFestivalsByDay(
public ResponseEntity<BasicResponse<PageResponse<FestivalInfo>>> getFestivalsByDay(
@AuthenticationPrincipal User user,
@RequestParam(name = "year") @NotNull int year,
@RequestParam(name = "month") @Min(1) @Max(12) @NotNull int month,
Expand All @@ -81,7 +83,7 @@ public ResponseEntity<BasicResponse<PageResponse<FestivalInfoResponse>>> getFest

String message = "페스티벌 일간 조회 성공";

Page<FestivalInfoResponse> festivalsByDay = festivalService.getFestivalsByDay(
Page<FestivalInfo> festivalsByDay = festivalService.getFestivalsByDay(
isNull(user) ? null : user.getId(), year, month, day, pageable);
return ResponseEntity.ok(BasicResponse.ok(message, PageResponse.of(festivalsByDay)));
}
Expand All @@ -91,7 +93,7 @@ public ResponseEntity<BasicResponse<PageResponse<FestivalInfoResponse>>> getFest
description = "필터와 정렬 조건을 사용하여 페스티벌을 다건 조회합니다."
)
@GetMapping("/filter")
public ResponseEntity<BasicResponse<PageResponse<FestivalInfoResponse>>> getFestivalsByFilters(
public ResponseEntity<BasicResponse<PageResponse<FestivalInfo>>> getFestivalsByFilters(
@AuthenticationPrincipal User user,
@ModelAttribute FestivalFilterRequest festivalFilterRequest,
@RequestParam(value = "lat", required = false) Double latitude,
Expand All @@ -100,12 +102,62 @@ public ResponseEntity<BasicResponse<PageResponse<FestivalInfoResponse>>> getFest

validateLatAndLng(latitude, longitude, pageable);

Page<FestivalInfoResponse> festivals = festivalService.getFestivalByFiltersAndSort(
Page<FestivalInfo> festivals = festivalService.getFestivalByFiltersAndSort(
isNull(user) ? null : user.getId(), festivalFilterRequest, latitude, longitude, pageable);

return ResponseEntity.ok(BasicResponse.ok("페스티벌 필터 조회 성공", PageResponse.of(festivals)));
}

@Operation(
summary = "페스티벌 이름 검색",
description = "페스티벌을 이름으로 검색합니다."
)
@GetMapping("/search")
public ResponseEntity<BasicResponse<PageResponse<FestivalInfo>>> getFestivalsByQuery(
@AuthenticationPrincipal User user,
String query,
@PageableDefault(size = 6) Pageable pageable) {

validateQuery(query);

Page<FestivalInfo> festivals = festivalService.getFestivalsByQuery(
isNull(user) ? null : user.getId(), query, pageable);

List<CompletableFuture<Void>> futures = festivals.getContent().stream()
.map(festival -> CompletableFuture.runAsync(() ->
festivalService.updateSearchRanking(
KEYWORD_RANKING_KEY,
FestivalBasic.of(festival.getFestivalId(), festival.getName())
)))
.toList();

return ResponseEntity.ok(BasicResponse.ok("페스티벌 이름 검색 성공", PageResponse.of(festivals)));
}

@Operation(
summary = "실시간 급상승 페스티벌 조회",
description = "실시간 급상승 페스티벌을 조회합니다."
)
@GetMapping("/trending")
public ResponseEntity<BasicResponse<PageResponse<FestivalBasic>>> getFestivalsByQuery(
@RequestParam(required = false, defaultValue = "0") Long page,
@RequestParam(required = false, defaultValue = "5") Integer size) {

PageResponse<FestivalBasic> trendingFestival = festivalService.getTrendingFestival(KEYWORD_RANKING_KEY, page,
size);

return ResponseEntity.ok(BasicResponse.ok("실시간 급상승 페스티벌 조회 성공", trendingFestival));
}

private void validateQuery(String query) {
if (isNull(query) || query.isEmpty()) {
throw new CustomException(QUERY_CANNOT_BE_EMPTY);
}

if (query.isBlank()) {
throw new CustomException(QUERY_CANNOT_BE_BLANK);
}
}

@Operation(
summary = "이번 주 페스티벌 조회",
Expand All @@ -118,7 +170,7 @@ public ResponseEntity<BasicResponse<PageResponse<FestivalThisWeekResponse>>> get
Page<FestivalThisWeekResponse> festivalsInThisWeek = festivalService.getFestivalsInThisWeek(pageable);
return ResponseEntity.ok(BasicResponse.ok("이번 주 페스티벌 조회 성공", PageResponse.of(festivalsInThisWeek)));
}

@Operation(summary = "페스티벌 북마크 등록/해제", description = "페스티벌 북마크를 등록 또는 해제합니다.")
@PatchMapping("/{festivalId}/bookmark")
public ResponseEntity<BasicResponse<FestivalBookmarkResponse>> updateFestivalBookmark(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@Tag(name = "Festival", description = "페스티벌 관련 API")
@Tag(name = "Festival Static Data", description = "페스티벌 관련 정적 데이터 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/festivals")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

@SuperBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ public class DailyFestivalContents {

@JsonFormat(shape = STRING, pattern = "yyyy-MM-dd")
private LocalDate date;
private List<FestivalBasicResponse> festivals;
private List<FestivalBasic> festivals;
private Integer totalElements;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.odiga.fiesta.festival.dto.response;

import com.odiga.fiesta.festival.domain.Festival;

import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class FestivalBasic {

private Long festivalId;
private String name;

public FestivalBasic(Long festivalId, String name) {
this.festivalId = festivalId;
this.name = name;
}

public static FestivalBasic of(Long festivalId, String name) {
return new FestivalBasic(festivalId, name);
}

public static FestivalBasic of(Festival festival) {
return new FestivalBasic(festival.getId(), festival.getName());
}
}

This file was deleted.

Loading

0 comments on commit 443599a

Please sign in to comment.