Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sim-mer-> weekly] 작가 검색 응답 팔로우 여부 추가 및 인기 검색어 관련 로직 작성 #83 #91

Merged
merged 9 commits into from
Nov 8, 2024
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies {
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Spring docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/helpmeCookies/Step3Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Step3Application {

public static void main(String[] args) {
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.helpmeCookies.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
public class RedisConfig {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import com.helpmeCookies.product.dto.ProductPage;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -20,10 +22,17 @@ public class ProductService {
private final ProductRepository productRepository;
private final ProductImageRepository productImageRepository;
private final ArtistInfoRepository artistInfoRepository;
private final RedisTemplate<String, Object> redisTemplate;

@Transactional(readOnly = true)
public ProductPage.Paging getProductsByPage(String query, Pageable pageable) {
var productPage = productRepository.findByNameWithIdx(query, pageable);

ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
var zSetKey = "search:" + query;
var time = System.currentTimeMillis();
zSet.add(zSetKey, String.valueOf(time), time);

return ProductPage.Paging.from(productPage);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.helpmeCookies.search.controller;

import com.helpmeCookies.search.dto.PopularSearchResponse;
import com.helpmeCookies.search.service.SearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/search")
public class SearchController {

private final SearchService searchService;

@GetMapping("/popular")
public ResponseEntity<PopularSearchResponse> getPopularSearch() {
return ResponseEntity.ok(searchService.getPopularSearch());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.helpmeCookies.search.dto;

import java.util.List;

public record PopularSearchResponse (
List<String> popularSearch
) {
}
90 changes: 90 additions & 0 deletions src/main/java/com/helpmeCookies/search/service/SearchService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.helpmeCookies.search.service;

import com.helpmeCookies.search.dto.PopularSearchResponse;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SearchService {

private final RedisTemplate<String, Object> redisTemplate;
private final String SEARCH_PREFIX = "search:";
private final String RANKING_KEY = "ranking";
private final int SEARCH_COUNT = 100;
private final double HOUR_WEIGHT = 0.15;
private final double DAY_WEIGHT = 0.12;
private final double WEEK_WEIGHT = 0.04;
private final double TOTAL_WEIGHT = 0.12;

@Scheduled(cron = "0 0 * * * *")
private void updatePopularSearch() {
List<String> keys = scanKeys();
Set<TypedTuple<Object>> values = new HashSet<>();

var current = System.currentTimeMillis();
long hour = current - TimeUnit.HOURS.toMillis(1);
long day = current - TimeUnit.DAYS.toMillis(1);
long week = current - TimeUnit.DAYS.toMillis(7);

for (var key : keys) {
values.add(TypedTuple.of(key.substring(SEARCH_PREFIX.length()),
calcScore(key, hour, current, day, week)));
}

redisTemplate.execute((RedisCallback<Object>) connection -> {
connection.multi();

redisTemplate.opsForZSet().removeRange(RANKING_KEY, 0, -1);
redisTemplate.opsForZSet().add(RANKING_KEY, values);

return connection.exec();
});
}

private double calcScore(String key, long hour, long current, long day, long week) {
var hourCount = redisTemplate.opsForZSet().count(key, hour, current);
var dayCount = redisTemplate.opsForZSet().count(key, day, current);
var weekCount = redisTemplate.opsForZSet().count(key, week, current);
var totalCount = redisTemplate.opsForZSet().size(key);

return (hourCount * HOUR_WEIGHT) + (dayCount * DAY_WEIGHT) + (weekCount * WEEK_WEIGHT) + (
totalCount * TOTAL_WEIGHT);
}

private List<String> scanKeys() {
List<String> keys = new ArrayList<>();
Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(redisConnection ->
redisConnection.scan(
ScanOptions.scanOptions().match(SEARCH_PREFIX).count(SEARCH_COUNT).build()
)
);

while (cursor.hasNext()) {
keys.add(new String(cursor.next()));
}

return keys;
}


public PopularSearchResponse getPopularSearch() {
Set<Object> result = redisTemplate.opsForZSet().range(RANKING_KEY, 0, 9);
List<String> stringResult = result.stream()
.map(Object::toString)
.toList();

return new PopularSearchResponse(stringResult);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,50 +23,52 @@
@RestController
@RequiredArgsConstructor
public class ArtistController implements ArtistApiDocs {
private final ArtistService artistService;
private final ArtistService artistService;

@PostMapping("/v1/artists/students")
public ResponseEntity<ApiResponse<Void>> registerStudents(
@RequestBody StudentArtistReq artistDetailsReq,
@AuthenticationPrincipal JwtUser jwtUser
) {
artistService.registerStudentsArtist(artistDetailsReq, jwtUser.getId());
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK)));
}
@PostMapping("/v1/artists/students")
public ResponseEntity<ApiResponse<Void>> registerStudents(
@RequestBody StudentArtistReq artistDetailsReq,
@AuthenticationPrincipal JwtUser jwtUser
) {
artistService.registerStudentsArtist(artistDetailsReq, jwtUser.getId());
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK)));
}

@PostMapping("/v1/artists/bussinesses")
public ResponseEntity<ApiResponse<Void>> registerbussinsess(
@RequestBody BusinessArtistReq businessArtistReq,
@AuthenticationPrincipal JwtUser jwtUser
) {
artistService.registerBusinessArtist(businessArtistReq, jwtUser.getId());
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK)));
}
@PostMapping("/v1/artists/bussinesses")
public ResponseEntity<ApiResponse<Void>> registerbussinsess(
@RequestBody BusinessArtistReq businessArtistReq,
@AuthenticationPrincipal JwtUser jwtUser
) {
artistService.registerBusinessArtist(businessArtistReq, jwtUser.getId());
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK)));
}

@GetMapping("/v1/artists/{userId}")
public ResponseEntity<ApiResponse<ArtistDetailsRes>> getArtist(
@PathVariable Long userId
) {
ArtistDetailsRes artistDetailsRes = artistService.getArtistDetails(userId);
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK, artistDetailsRes)));
}
@GetMapping("/v1/artists/{userId}")
public ResponseEntity<ApiResponse<ArtistDetailsRes>> getArtist(
@PathVariable Long userId
) {
ArtistDetailsRes artistDetailsRes = artistService.getArtistDetails(userId);
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK, artistDetailsRes)));
}

@GetMapping("/v1/artist")
public ResponseEntity<ApiResponse<ArtistDetailsRes>> getArtist(
@AuthenticationPrincipal JwtUser jwtUser
) {
ArtistDetailsRes artistDetailsRes = artistService.getArtistDetails(jwtUser.getId());
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK, artistDetailsRes)));
}
@GetMapping("/v1/artist")
public ResponseEntity<ApiResponse<ArtistDetailsRes>> getArtist(
@AuthenticationPrincipal JwtUser jwtUser
) {
ArtistDetailsRes artistDetailsRes = artistService.getArtistDetails(jwtUser.getId());
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK, artistDetailsRes)));
}

@GetMapping("/v1/artists")
public ResponseEntity<ApiResponse<ArtistInfoPage.Paging>> getArtistsByPage(
@RequestParam("query") String query,
@RequestParam(name = "size", required = false, defaultValue = "20") int size,
@RequestParam("page") int page
) {
var pageable = PageRequest.of(page, size);
ArtistInfoPage.Paging artistInfoPage = artistService.getArtistsByPage(query, pageable);
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK, artistInfoPage)));
}
@GetMapping("/v1/artists/search")
public ResponseEntity<ApiResponse<ArtistInfoPage.Paging>> getArtistsByPage(
@RequestParam("query") String query,
@RequestParam(name = "size", required = false, defaultValue = "20") int size,
@RequestParam("page") int page,
@AuthenticationPrincipal JwtUser jwtUser
) {
var pageable = PageRequest.of(page, size);
var userId = jwtUser == null ? null : jwtUser.getId();
ArtistInfoPage.Paging artistInfoPage = artistService.getArtistsByPage(query, pageable, userId);
return ResponseEntity.ok((ApiResponse.success(SuccessCode.OK, artistInfoPage)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.helpmeCookies.global.ApiResponse.ApiResponse;
import com.helpmeCookies.global.jwt.JwtUser;
import com.helpmeCookies.user.dto.ArtistInfoPage;
import com.helpmeCookies.user.dto.ArtistInfoPage.Paging;
import com.helpmeCookies.user.dto.request.BusinessArtistReq;
import com.helpmeCookies.user.dto.request.StudentArtistReq;
import com.helpmeCookies.user.dto.response.ArtistDetailsRes;
Expand Down Expand Up @@ -50,6 +49,7 @@ ResponseEntity<ApiResponse<ArtistDetailsRes>> getArtist(
ResponseEntity<ApiResponse<ArtistInfoPage.Paging>> getArtistsByPage(
String query,
@Parameter(description = "default value 20") int size,
int page
int page,
@Parameter(hidden = true) JwtUser jwtUser
);
}
17 changes: 10 additions & 7 deletions src/main/java/com/helpmeCookies/user/dto/ArtistInfoPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.helpmeCookies.user.repository.dto.ArtistSearch;
import java.util.List;
import java.util.Set;
import org.springframework.data.domain.Page;

public class ArtistInfoPage {
Expand All @@ -11,22 +12,24 @@ public record Info(
String nickname,
String artistImageUrl,
Long totalFollowers,
Long totalLikes
Long totalLikes,
Boolean isFollowing
) {

private static Info from(ArtistSearch artistSearch) {
private static Info from(ArtistSearch artistSearch, boolean isFollowing) {
return new Info(
artistSearch.getId(),
artistSearch.getNickname(),
artistSearch.getArtistImageUrl(),
artistSearch.getTotalFollowers(),
artistSearch.getTotalLikes()
artistSearch.getTotalLikes(),
isFollowing
);
}

public static List<Info> of(List<ArtistSearch> content) {
public static List<Info> of(List<ArtistSearch> content, Set<Long> followingIds) {
return content.stream()
.map(Info::from)
.map(artistSearch -> from(artistSearch, followingIds.contains(artistSearch.getId())))
.toList();
}
}
Expand All @@ -35,10 +38,10 @@ public record Paging (
boolean hasNext,
List<Info> artists
) {
public static Paging from(Page<ArtistSearch> artistPage) {
public static Paging of(Page<ArtistSearch> artistPage, Set<Long> followingIds) {
return new Paging(
artistPage.hasNext(),
Info.of(artistPage.getContent())
Info.of(artistPage.getContent(), followingIds)
);
}
}
Expand Down
Loading