Skip to content

Commit abe8e24

Browse files
authored
Merge pull request #124 from JA-yeong-eop-JA-moeu-JA/feat/#122-recent-keywords
✨ Feat: 최근 검색어 관련 API 구현
2 parents bcb5320 + 2371873 commit abe8e24

File tree

6 files changed

+131
-5
lines changed

6 files changed

+131
-5
lines changed

src/main/java/com/jajaja/domain/search/controller/SearchController.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import com.jajaja.domain.product.dto.response.ProductListResponseDto;
44
import com.jajaja.domain.search.dto.PopularSearchKeywordsResponseDto;
5+
import com.jajaja.domain.search.dto.RecentSearchKeywordResponseDto;
56
import com.jajaja.domain.search.entity.enums.SearchSort;
67
import com.jajaja.domain.search.service.SearchService;
78
import com.jajaja.global.apiPayload.ApiResponse;
9+
import com.jajaja.global.security.annotation.Auth;
810
import io.swagger.v3.oas.annotations.Operation;
911
import jakarta.validation.constraints.NotBlank;
1012
import lombok.RequiredArgsConstructor;
@@ -26,11 +28,11 @@ public class SearchController {
2628
)
2729
@GetMapping
2830
public ApiResponse<List<ProductListResponseDto>> searchProducts(
29-
@RequestParam
30-
@NotBlank(message = "검색어를 입력해주세요.") String keyword,
31+
@Auth Long memberId,
32+
@RequestParam @NotBlank(message = "검색어를 입력해주세요.") String keyword,
3133
@RequestParam(defaultValue = "POPULAR") SearchSort sort) {
3234

33-
List<ProductListResponseDto> result = searchService.searchProductsByKeyword(keyword, sort);
35+
List<ProductListResponseDto> result = searchService.searchProductsByKeyword(memberId, keyword, sort);
3436
return ApiResponse.onSuccess(result);
3537
}
3638

@@ -42,4 +44,25 @@ public ApiResponse<PopularSearchKeywordsResponseDto> getPopularKeywords() {
4244
PopularSearchKeywordsResponseDto popularKeywords = searchService.getPopularKeywords();
4345
return ApiResponse.onSuccess(popularKeywords);
4446
}
47+
48+
@Operation(summary = "최근 검색어 조회 API | by 구름/윤윤지",
49+
description = "회원의 최근 검색어를 조회합니다. (ID 포함)"
50+
)
51+
@GetMapping("/recent-keywords")
52+
public ApiResponse<List<RecentSearchKeywordResponseDto>> getRecentSearchKeywords(@Auth Long memberId) {
53+
List<RecentSearchKeywordResponseDto> keywords = searchService.getRecentSearchKeywords(memberId);
54+
return ApiResponse.onSuccess(keywords);
55+
}
56+
57+
@Operation(summary = "최근 검색어 삭제 API | by 구름/윤윤지",
58+
description = "최근 검색어 중 특정 검색어를 삭제합니다."
59+
)
60+
@DeleteMapping("/recent-keywords/{keywordId}")
61+
public ApiResponse<Void> deleteSearchKeyword(
62+
@Auth Long memberId,
63+
@PathVariable Long keywordId
64+
) {
65+
searchService.deleteSearchKeywordById(memberId, keywordId);
66+
return ApiResponse.onSuccess(null);
67+
}
4568
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.jajaja.domain.search.dto;
2+
3+
public record RecentSearchKeywordResponseDto(Long id, String keyword) {
4+
public static RecentSearchKeywordResponseDto of(Long id, String keyword) {
5+
return new RecentSearchKeywordResponseDto(id, keyword);
6+
}
7+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.jajaja.domain.search.entity;
2+
3+
import com.jajaja.domain.member.entity.Member;
4+
import com.jajaja.global.common.domain.BaseEntity;
5+
import jakarta.persistence.*;
6+
import lombok.*;
7+
8+
@Entity
9+
@Getter
10+
@Builder
11+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
12+
@AllArgsConstructor
13+
public class MemberSearchHistory extends BaseEntity {
14+
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
@ManyToOne(fetch = FetchType.LAZY)
20+
private Member member;
21+
22+
@Column(nullable = false, length = 100)
23+
private String keyword;
24+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.jajaja.domain.search.repository;
2+
3+
import com.jajaja.domain.search.entity.MemberSearchHistory;
4+
import com.jajaja.domain.member.entity.Member;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
import java.util.List;
8+
import java.util.Optional;
9+
10+
public interface MemberSearchHistoryRepository extends JpaRepository<MemberSearchHistory, Long> {
11+
List<MemberSearchHistory> findTop10ByMemberOrderByCreatedAtDesc(Member member);
12+
Optional<MemberSearchHistory> findByMemberAndKeyword(Member member, String keyword);
13+
List<MemberSearchHistory> findByMemberOrderByCreatedAtDesc(Member member);
14+
Optional<MemberSearchHistory> findByMemberAndId(Member member, Long id);
15+
}

src/main/java/com/jajaja/domain/search/service/SearchService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.jajaja.domain.product.dto.response.ProductListResponseDto;
44
import com.jajaja.domain.search.dto.PopularSearchKeywordsResponseDto;
5+
import com.jajaja.domain.search.dto.RecentSearchKeywordResponseDto;
56
import com.jajaja.domain.search.entity.enums.SearchSort;
67

78
import java.util.List;
89

910
public interface SearchService {
10-
List<ProductListResponseDto> searchProductsByKeyword(String keyword, SearchSort sort);
1111
PopularSearchKeywordsResponseDto getPopularKeywords();
12+
List<ProductListResponseDto> searchProductsByKeyword(Long memberId, String keyword, SearchSort sort);
13+
List<RecentSearchKeywordResponseDto> getRecentSearchKeywords(Long memberId);
14+
void deleteSearchKeywordById(Long memberId, Long keywordId);
1215
}

src/main/java/com/jajaja/domain/search/service/SearchServiceImpl.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.jajaja.domain.search.service;
22

3+
import com.jajaja.domain.member.entity.Member;
4+
import com.jajaja.domain.member.repository.MemberRepository;
35
import com.jajaja.domain.product.converter.ProductConverter;
46
import com.jajaja.domain.product.dto.response.ProductListResponseDto;
57
import com.jajaja.domain.product.entity.Product;
68
import com.jajaja.domain.product.repository.ProductSalesRepository;
79
import com.jajaja.domain.search.dto.PopularSearchKeywordsResponseDto;
10+
import com.jajaja.domain.search.dto.RecentSearchKeywordResponseDto;
11+
import com.jajaja.domain.search.entity.MemberSearchHistory;
812
import com.jajaja.domain.search.entity.Search;
913
import com.jajaja.domain.search.entity.enums.SearchSort;
14+
import com.jajaja.domain.search.repository.MemberSearchHistoryRepository;
1015
import com.jajaja.domain.search.repository.SearchRepository;
1116
import com.jajaja.global.apiPayload.code.status.ErrorStatus;
1217
import com.jajaja.global.apiPayload.exception.custom.BadRequestException;
@@ -26,23 +31,32 @@ public class SearchServiceImpl implements SearchService {
2631
private final SearchRepository searchRepository;
2732
private final ProductConverter productConverter;
2833
private final ProductSalesRepository productSalesRepository;
34+
private final MemberRepository memberRepository;
35+
private final MemberSearchHistoryRepository memberSearchHistoryRepository;
2936

3037
/**
3138
* 키워드 기반 상품 검색
3239
*
40+
* @param memberId 로그인한 회원 ID (null 허용)
3341
* @param keyword 검색어
3442
* @param sort 정렬 기준 (POPULAR, NEW, LOW_PRICE, REVIEW)
3543
* @return 상품 리스트 DTO
3644
*/
3745
@Override
3846
@Transactional
39-
public List<ProductListResponseDto> searchProductsByKeyword(String keyword, SearchSort sort) {
47+
public List<ProductListResponseDto> searchProductsByKeyword(Long memberId, String keyword, SearchSort sort) {
4048
if (keyword == null || keyword.isBlank()) {
4149
throw new BadRequestException(ErrorStatus.INVALID_KEYWORD);
4250
}
4351

4452
saveOrIncrementKeyword(keyword);
4553

54+
if (memberId != null) {
55+
Member member = memberRepository.findById(memberId)
56+
.orElseThrow(() -> new BadRequestException(ErrorStatus.MEMBER_NOT_FOUND));
57+
saveMemberSearchKeyword(member, keyword);
58+
}
59+
4660
List<Product> products = searchRepository.findProductsByKeyword(keyword);
4761
if (products.isEmpty()) {
4862
return Collections.emptyList();
@@ -79,6 +93,45 @@ protected void saveOrIncrementKeyword(String keyword) {
7993
);
8094
}
8195

96+
@Transactional
97+
public void saveMemberSearchKeyword(Member member, String keyword) {
98+
memberSearchHistoryRepository.findByMemberAndKeyword(member, keyword)
99+
.ifPresent(memberSearchHistoryRepository::delete);
100+
101+
memberSearchHistoryRepository.save(MemberSearchHistory.builder()
102+
.member(member)
103+
.keyword(keyword)
104+
.build());
105+
106+
List<MemberSearchHistory> histories = memberSearchHistoryRepository.findByMemberOrderByCreatedAtDesc(member);
107+
if (histories.size() > 10) {
108+
memberSearchHistoryRepository.deleteAll(histories.subList(10, histories.size()));
109+
}
110+
}
111+
112+
@Override
113+
public List<RecentSearchKeywordResponseDto> getRecentSearchKeywords(Long memberId) {
114+
Member member = memberRepository.findById(memberId)
115+
.orElseThrow(() -> new BadRequestException(ErrorStatus.MEMBER_NOT_FOUND));
116+
117+
return memberSearchHistoryRepository.findTop10ByMemberOrderByCreatedAtDesc(member)
118+
.stream()
119+
.map(h -> RecentSearchKeywordResponseDto.of(h.getId(), h.getKeyword()))
120+
.toList();
121+
}
122+
123+
@Override
124+
@Transactional
125+
public void deleteSearchKeywordById(Long memberId, Long keywordId) {
126+
Member member = memberRepository.findById(memberId)
127+
.orElseThrow(() -> new BadRequestException(ErrorStatus.MEMBER_NOT_FOUND));
128+
129+
memberSearchHistoryRepository.findByMemberAndId(member, keywordId)
130+
.ifPresent(memberSearchHistoryRepository::delete);
131+
}
132+
133+
134+
82135
private void sortProducts(List<Product> products, SearchSort sort) {
83136
switch (sort) {
84137
case NEW:
@@ -120,4 +173,5 @@ private void sortByTotalSales(List<Product> products) {
120173
products.sort((p1, p2) -> salesMap.getOrDefault(p2.getId(), 0L)
121174
.compareTo(salesMap.getOrDefault(p1.getId(), 0L)));
122175
}
176+
123177
}

0 commit comments

Comments
 (0)