Skip to content

Commit 7a71f58

Browse files
authored
Merge pull request #7 from Qua-Project/feat/recentSearch
feat : 최근 검색어 기능 추가
2 parents a154abe + 0ecc7bf commit 7a71f58

File tree

8 files changed

+137
-19
lines changed

8 files changed

+137
-19
lines changed

src/main/java/medilux/aquabe/domain/product/controller/ProductController.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import medilux.aquabe.domain.product.service.ProductService;
77
import medilux.aquabe.domain.search.service.SearchLogService;
88
import org.springframework.http.ResponseEntity;
9+
import org.springframework.security.core.Authentication;
10+
import org.springframework.security.core.context.SecurityContextHolder;
911
import org.springframework.web.bind.annotation.*;
1012

1113
import java.util.List;
@@ -22,15 +24,18 @@ public class ProductController {
2224
// 제품 검색 이름 API
2325
@GetMapping("/search")
2426
public ResponseEntity<List<ProductSearchResponse>> searchProducts(
25-
@RequestParam(name = "query", required = false) String query,
27+
@RequestParam(name = "keyword", required = false) String keyword,
2628
@RequestParam(name = "category", required = false) Integer category,
2729
@RequestParam(name = "type", required = false) String type) {
28-
29-
if (query != null && !query.trim().isEmpty()) {
30-
searchKeywordService.saveSearchKeyword(query);
30+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
31+
String loginEmail = authentication.getName();
32+
//검색어 저장
33+
if (keyword != null && !keyword.trim().isEmpty()) {
34+
searchKeywordService.saveSearchKeyword(loginEmail, keyword);
3135
}
3236

33-
List<ProductSearchResponse> products = productService.searchProducts(query, category, type);
37+
38+
List<ProductSearchResponse> products = productService.searchProducts(keyword, category, type);
3439
return ResponseEntity.ok(products);
3540
}
3641

src/main/java/medilux/aquabe/domain/search/controller/SearchKeywordController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import lombok.RequiredArgsConstructor;
44
import medilux.aquabe.domain.search.dto.PopularKeywordResponse;
5+
import medilux.aquabe.domain.search.dto.RecentKeywordResponse;
56
import medilux.aquabe.domain.search.service.SearchLogService;
67
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.context.SecurityContextHolder;
710
import org.springframework.web.bind.annotation.*;
811

912
import java.util.List;
@@ -19,4 +22,11 @@ public class SearchKeywordController {
1922
public ResponseEntity<List<PopularKeywordResponse>> getPopularKeywords() {
2023
return ResponseEntity.ok(searchLogService.getTop10SearchKeywords());
2124
}
25+
26+
@GetMapping("/recent")
27+
public ResponseEntity<RecentKeywordResponse> getRecentKeywords(){
28+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
29+
String loginEmail = authentication.getName();
30+
return ResponseEntity.ok(searchLogService.getRecentSearchKeywords(loginEmail));
31+
}
2232
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package medilux.aquabe.domain.search.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Builder
10+
public class RecentKeywordResponse {
11+
private List<String> keyword;
12+
}

src/main/java/medilux/aquabe/domain/search/entity/SearchLogEntity.java renamed to src/main/java/medilux/aquabe/domain/search/entity/SearchLogPopularEntity.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
@Entity
88
@Getter
9-
@Table(name = "search_logs")
9+
@Table(name = "search_logs_popular")
1010
@NoArgsConstructor(access = AccessLevel.PROTECTED)
11-
public class SearchLogEntity {
11+
public class SearchLogPopularEntity {
1212

1313
@Id
1414
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -21,7 +21,7 @@ public class SearchLogEntity {
2121
private LocalDateTime searchedAt; // 검색된 시간 저장
2222

2323
@Builder
24-
public SearchLogEntity(String keyword) {
24+
public SearchLogPopularEntity(String keyword) {
2525
this.keyword = keyword;
2626
this.searchedAt = LocalDateTime.now();
2727
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package medilux.aquabe.domain.search.entity;
2+
3+
4+
import jakarta.persistence.*;
5+
import lombok.AccessLevel;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
import medilux.aquabe.domain.user.entity.UserEntity;
10+
import org.hibernate.annotations.OnDelete;
11+
import org.hibernate.annotations.OnDeleteAction;
12+
13+
import java.time.LocalDateTime;
14+
15+
@Entity
16+
@Getter
17+
@Table(name = "search_logs_recent")
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
19+
public class SearchLogRecentEntity {
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
private Long id;
23+
24+
@Column(nullable = false)
25+
private String keyword;
26+
27+
@Column(nullable = false)
28+
private LocalDateTime searchedAt; // 검색된 시간 저장
29+
30+
@ManyToOne
31+
@JoinColumn(name = "user_id", nullable = false)
32+
@OnDelete(action = OnDeleteAction.CASCADE) // User 삭제 시 해당 검색어 삭제
33+
private UserEntity user;
34+
35+
36+
@Builder
37+
public SearchLogRecentEntity(String keyword, UserEntity user) {
38+
this.keyword = keyword;
39+
this.searchedAt = LocalDateTime.now();
40+
this.user = user;
41+
}
42+
43+
44+
45+
46+
}

src/main/java/medilux/aquabe/domain/search/repository/SearchLogRepository.java renamed to src/main/java/medilux/aquabe/domain/search/repository/SearchLogPopularRepository.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package medilux.aquabe.domain.search.repository;
22

3-
import medilux.aquabe.domain.search.entity.SearchLogEntity;
3+
import medilux.aquabe.domain.search.entity.SearchLogPopularEntity;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.data.jpa.repository.Query;
66
import org.springframework.data.repository.query.Param;
77
import java.time.LocalDateTime;
88
import java.util.List;
99

10-
public interface SearchLogRepository extends JpaRepository<SearchLogEntity, Long> {
10+
public interface SearchLogPopularRepository extends JpaRepository<SearchLogPopularEntity, Long> {
1111

1212
// 특정 시간 이후의 검색 로그만 가져오기
13-
List<SearchLogEntity> findBySearchedAtAfter(LocalDateTime time);
13+
List<SearchLogPopularEntity> findBySearchedAtAfter(LocalDateTime time);
1414

1515
// 특정 시간 이전의 검색 로그 삭제
1616
void deleteBySearchedAtBefore(LocalDateTime time);
1717

1818
// 최근 N시간 동안 가장 많이 검색된 키워드 (상위 10개)
19-
@Query("SELECT s.keyword, COUNT(s.keyword) as count FROM SearchLogEntity s " +
19+
@Query("SELECT s.keyword, COUNT(s.keyword) as count FROM SearchLogPopularEntity s " +
2020
"WHERE s.searchedAt > :time " +
2121
"GROUP BY s.keyword " +
2222
"ORDER BY count DESC")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package medilux.aquabe.domain.search.repository;
2+
3+
import medilux.aquabe.domain.search.entity.SearchLogPopularEntity;
4+
import medilux.aquabe.domain.search.entity.SearchLogRecentEntity;
5+
import medilux.aquabe.domain.user.entity.UserEntity;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
import java.util.List;
9+
10+
public interface SearchLogRecentRepository extends JpaRepository<SearchLogRecentEntity, Long> {
11+
12+
List<SearchLogRecentEntity> findByUser(UserEntity user);
13+
}
Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package medilux.aquabe.domain.search.service;
22

33
import lombok.RequiredArgsConstructor;
4+
import medilux.aquabe.common.error.exceptions.BadRequestException;
45
import medilux.aquabe.domain.search.dto.PopularKeywordResponse;
5-
import medilux.aquabe.domain.search.entity.SearchLogEntity;
6-
import medilux.aquabe.domain.search.repository.SearchLogRepository;
6+
import medilux.aquabe.domain.search.dto.RecentKeywordResponse;
7+
import medilux.aquabe.domain.search.entity.SearchLogPopularEntity;
8+
import medilux.aquabe.domain.search.entity.SearchLogRecentEntity;
9+
import medilux.aquabe.domain.search.repository.SearchLogPopularRepository;
10+
import medilux.aquabe.domain.search.repository.SearchLogRecentRepository;
11+
import medilux.aquabe.domain.user.entity.UserEntity;
12+
import medilux.aquabe.domain.user.repository.UserRepository;
713
import org.springframework.scheduling.annotation.Scheduled;
814
import org.springframework.stereotype.Service;
915
import org.springframework.transaction.annotation.Transactional;
@@ -13,22 +19,32 @@
1319
import java.util.stream.Collectors;
1420
import java.util.stream.IntStream;
1521

22+
import static medilux.aquabe.common.error.ErrorCode.ROW_DOES_NOT_EXIST;
23+
1624
@Service
1725
@RequiredArgsConstructor
1826
public class SearchLogService {
1927

20-
private final SearchLogRepository searchLogRepository;
28+
private final SearchLogPopularRepository searchLogPopularRepository;
29+
private final SearchLogRecentRepository searchLogRecentRepository;
30+
private final UserRepository userRepository;
2131

2232
// 검색어 저장
2333
@Transactional
24-
public void saveSearchKeyword(String keyword) {
25-
searchLogRepository.save(new SearchLogEntity(keyword));
34+
public void saveSearchKeyword(String loginEmail, String keyword) {
35+
36+
UserEntity user = userRepository.findByEmail(loginEmail)
37+
.orElseThrow(() -> new BadRequestException(ROW_DOES_NOT_EXIST, "존재하지 않는 사용자입니다."));
38+
39+
searchLogRecentRepository.save(new SearchLogRecentEntity(keyword, user));
40+
searchLogPopularRepository.save(new SearchLogPopularEntity(keyword));
2641
}
2742

2843
// 인기 검색어 조회 (24시간 기준)
44+
@Transactional(readOnly = true)
2945
public List<PopularKeywordResponse> getTop10SearchKeywords() {
3046
LocalDateTime recentTime = LocalDateTime.now().minusHours(24);
31-
List<Object[]> topKeywords = searchLogRepository.findTop10PopularKeywords(recentTime);
47+
List<Object[]> topKeywords = searchLogPopularRepository.findTop10PopularKeywords(recentTime);
3248

3349
return IntStream.range(0, topKeywords.size())
3450
.mapToObj(i -> PopularKeywordResponse.builder()
@@ -38,11 +54,27 @@ public List<PopularKeywordResponse> getTop10SearchKeywords() {
3854
.collect(Collectors.toList());
3955
}
4056

57+
@Transactional(readOnly = true)
58+
public RecentKeywordResponse getRecentSearchKeywords(String loginEmail){
59+
UserEntity user = userRepository.findByEmail(loginEmail)
60+
.orElseThrow(() -> new BadRequestException(ROW_DOES_NOT_EXIST, "존재하지 않는 사용자입니다."));
61+
62+
List<SearchLogRecentEntity> searchLogRecent = searchLogRecentRepository.findByUser(user);
63+
64+
List<String> keywords = searchLogRecent.stream()
65+
.map(SearchLogRecentEntity::getKeyword)
66+
.toList();
67+
68+
return RecentKeywordResponse.builder()
69+
.keyword(keywords)
70+
.build();
71+
}
72+
4173
// 오래된 검색어 삭제
4274
@Transactional
4375
@Scheduled(cron = "0 0 * * * ?") // 1시간마다 삭제 실행
4476
public void deleteOldSearchLogs() {
4577
LocalDateTime thresholdTime = LocalDateTime.now().minusHours(24);
46-
searchLogRepository.deleteBySearchedAtBefore(thresholdTime);
78+
searchLogPopularRepository.deleteBySearchedAtBefore(thresholdTime);
4779
}
4880
}

0 commit comments

Comments
 (0)