-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #70 from jisung-in/feature/67-real-time-search-ter…
…m-api [Feature] 실시간 검색어 API 구현
- Loading branch information
Showing
7 changed files
with
277 additions
and
8 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
src/main/java/com/jisungin/api/search/SearchController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.jisungin.api.search; | ||
|
||
import com.jisungin.api.ApiResponse; | ||
import com.jisungin.api.search.request.SearchKeywordRequest; | ||
import com.jisungin.application.search.SearchService; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import java.util.List; | ||
|
||
@Slf4j | ||
@RequestMapping("/v1/search") | ||
@RequiredArgsConstructor | ||
@RestController | ||
public class SearchController { | ||
|
||
private final SearchService searchService; | ||
|
||
@PostMapping("/rank") | ||
public ApiResponse<Void> addScoreSearchKeyword(@ModelAttribute @Valid SearchKeywordRequest request) { | ||
log.info("키워드 = {}", request.getKeyword()); | ||
searchService.searchKeyword(request.getKeyword()); | ||
return ApiResponse.ok(null); | ||
} | ||
|
||
@GetMapping("/rank") | ||
public ApiResponse<List<String>> getSearchRanking() { | ||
return ApiResponse.ok(searchService.getRankKeywords()); | ||
} | ||
|
||
} |
16 changes: 16 additions & 0 deletions
16
src/main/java/com/jisungin/api/search/request/SearchKeywordRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.jisungin.api.search.request; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
@NoArgsConstructor | ||
public class SearchKeywordRequest { | ||
|
||
@NotBlank(message = "키워드 값은 필수 입니다.") | ||
private String keyword; | ||
|
||
} |
33 changes: 33 additions & 0 deletions
33
src/main/java/com/jisungin/application/search/SearchService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.jisungin.application.search; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Qualifier; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.core.ZSetOperations; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.util.List; | ||
import java.util.Set; | ||
|
||
@Slf4j | ||
@Service | ||
public class SearchService { | ||
|
||
private final RedisTemplate<String, String> redisTemplate; | ||
|
||
public SearchService(@Qualifier("redisTemplateSecond") RedisTemplate<String, String> redisTemplate) { | ||
this.redisTemplate = redisTemplate; | ||
} | ||
|
||
public void searchKeyword(String keyword) { | ||
ZSetOperations<String, String> zset = redisTemplate.opsForZSet(); | ||
zset.incrementScore("ranking", keyword, 1); // 점수 증가 | ||
} | ||
|
||
public List<String> getRankKeywords() { | ||
ZSetOperations<String, String> zset = redisTemplate.opsForZSet(); | ||
Set<String> typedTuples = zset.reverseRange("ranking", 0, 9); | ||
return List.copyOf(typedTuples); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
src/test/java/com/jisungin/api/search/SearchControllerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package com.jisungin.api.search; | ||
|
||
import com.jisungin.ControllerTestSupport; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.http.MediaType; | ||
|
||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; | ||
|
||
class SearchControllerTest extends ControllerTestSupport { | ||
|
||
@DisplayName("검색한 키워드의 점수를 증가시킨다.") | ||
@Test | ||
void addScoreSearchKeyword() throws Exception { | ||
//given | ||
//when //then | ||
mockMvc.perform( | ||
post("/v1/search/rank?keyword=정의") | ||
.contentType(MediaType.APPLICATION_JSON)) | ||
.andExpect(status().isOk()) | ||
.andExpect(jsonPath("$.code").value("200")) | ||
.andExpect(jsonPath("$.status").value("OK")) | ||
.andExpect(jsonPath("$.message").value("OK")) | ||
.andDo(print()); | ||
} | ||
|
||
@DisplayName("검색한 키워드가 null이면 예외가 발생한다.") | ||
@Test | ||
void addScoreSearchKeywordWithNull() throws Exception { | ||
//given | ||
//when //then | ||
mockMvc.perform( | ||
post("/v1/search/rank") | ||
.contentType(MediaType.APPLICATION_JSON)) | ||
.andExpect(status().isBadRequest()) | ||
.andExpect(jsonPath("$.code").value("400")) | ||
.andExpect(jsonPath("$.message").value("키워드 값은 필수 입니다.")) | ||
.andDo(print()); | ||
} | ||
|
||
@DisplayName("검색한 키워드가 공백이면 예외가 발생한다.") | ||
@Test | ||
void addScoreSearchKeywordWithEmpty() throws Exception { | ||
//given | ||
//when //then | ||
mockMvc.perform( | ||
post("/v1/search/rank?keyword=") | ||
.contentType(MediaType.APPLICATION_JSON)) | ||
.andExpect(status().isBadRequest()) | ||
.andExpect(jsonPath("$.code").value("400")) | ||
.andExpect(jsonPath("$.message").value("키워드 값은 필수 입니다.")) | ||
.andDo(print()); | ||
} | ||
|
||
@DisplayName("인기 검색어 랭킹을 조회한다.") | ||
@Test | ||
void getSearchRanking() throws Exception { | ||
//given | ||
//when //then | ||
mockMvc.perform(get("/v1/search/rank") | ||
.contentType(MediaType.APPLICATION_JSON) | ||
) | ||
.andExpect(status().isOk()) | ||
.andExpect(jsonPath("$.code").value("200")) | ||
.andExpect(jsonPath("$.status").value("OK")) | ||
.andExpect(jsonPath("$.message").value("OK")) | ||
.andDo(print()); | ||
} | ||
|
||
} |
63 changes: 63 additions & 0 deletions
63
src/test/java/com/jisungin/application/search/SearchServiceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package com.jisungin.application.search; | ||
|
||
import com.jisungin.RedisTestContainer; | ||
import com.jisungin.infra.s3.S3FileManager; | ||
import org.assertj.core.api.Assertions; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Qualifier; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.boot.test.mock.mockito.MockBean; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.core.ZSetOperations; | ||
import org.springframework.test.context.event.RecordApplicationEvents; | ||
|
||
import java.util.List; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
@SpringBootTest | ||
@RecordApplicationEvents | ||
public class SearchServiceTest extends RedisTestContainer { | ||
|
||
@Autowired | ||
private SearchService searchService; | ||
|
||
@Autowired | ||
private @Qualifier("redisTemplateSecond") RedisTemplate<String, String> redisTemplate; | ||
|
||
@MockBean | ||
private S3FileManager s3FileManager; | ||
|
||
@DisplayName("사용자가 검색한 키워드의 점수가 1 증가한다.") | ||
@org.junit.jupiter.api.Test | ||
void searchSaveRanking() { | ||
//given | ||
String keyword = "testKeyword"; | ||
|
||
//when | ||
searchService.searchKeyword(keyword); | ||
|
||
//then | ||
ZSetOperations<String, String> zset = redisTemplate.opsForZSet(); | ||
Double score = zset.score("ranking", keyword); | ||
assertThat(score).isEqualTo(1.0); // 검색어의 점수가 1.0인지 확인 | ||
|
||
} | ||
|
||
@DisplayName("키워드 검색 횟수 상위 10개를 가져온다.") | ||
@Test | ||
void getRankKeywords() { | ||
//given | ||
String keyword = "testKeyword"; | ||
|
||
//when | ||
searchService.searchKeyword(keyword); | ||
|
||
//then | ||
List<String> rankKeywords = searchService.getRankKeywords(); | ||
Assertions.assertThat(rankKeywords).contains(keyword); // 랭킹에 추가된 검색어가 있는지 확인 | ||
} | ||
|
||
} |