-
Notifications
You must be signed in to change notification settings - Fork 1
✨ feat/product/KIKI-61-Search : 검색관련 기능 고도화 #65
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
Changes from all commits
1246bab
b029571
e680d73
7a8bc2a
aec34e7
8e0b425
0da16c4
c7659e9
3501e99
3797aab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,130 @@ | ||
| package site.kikihi.custom.platform.adapter.in.web; | ||
|
|
||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import site.kikihi.custom.global.response.ApiResponse; | ||
| import site.kikihi.custom.global.response.page.PageRequest; | ||
| import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; | ||
| import site.kikihi.custom.platform.application.service.ElasticSearchService; | ||
| import site.kikihi.custom.platform.adapter.in.web.dto.response.search.SearchListResponse; | ||
| import site.kikihi.custom.platform.adapter.in.web.swagger.SearchControllerSpec; | ||
| import site.kikihi.custom.platform.application.in.search.SearchUseCase; | ||
| import site.kikihi.custom.platform.domain.product.Product; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import site.kikihi.custom.platform.domain.search.Search; | ||
| import site.kikihi.custom.security.oauth2.domain.PrincipalDetails; | ||
|
|
||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/search") | ||
| @RequiredArgsConstructor | ||
| public class SearchController { | ||
| public class SearchController implements SearchControllerSpec { | ||
|
|
||
| private final ElasticSearchService searchService; | ||
| private final SearchUseCase service; | ||
|
|
||
| // 상품 검색 | ||
| /// 상품 검색 | ||
| @GetMapping | ||
| public ApiResponse<List<ProductListResponse>> searchProducts( | ||
| @RequestParam("keyword") String keyword, | ||
| @RequestParam(defaultValue = "0") int page, | ||
| @RequestParam(defaultValue = "20") int size, | ||
| @RequestParam(defaultValue = "0.001") float minScore | ||
| PageRequest pageRequest, | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ) { | ||
|
|
||
| List<Product> productList = searchService.searchProducts(keyword, page, size, minScore); | ||
| /// 유저가 없다면 null 저장 | ||
| UUID userId = principalDetails != null ? principalDetails.getId() : null; | ||
|
|
||
| /// 서비스 호출 | ||
| List<Product> productList = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId); | ||
|
|
||
| /// DTO 수정 | ||
| List<ProductListResponse> responses = ProductListResponse.from(productList); | ||
|
|
||
| /// 응답 | ||
| return ApiResponse.ok(responses); | ||
| } | ||
|
|
||
| // 상품 필터링 | ||
| // @GetMapping("/filter") | ||
| // public ApiResponse<List<Product>> filterProducts( | ||
| // @RequestParam("keyword") String keyword, | ||
| // @RequestBody SearchRequest req, | ||
| // PageRequest pageRequest | ||
| // ) { | ||
| // List<Product> products = searchService.filterProducts(keyword, req.manufacturer(), req.minPrice(), req.maxPrice(), pageRequest.getPage(), pageRequest.getSize()); | ||
| // return ApiResponse.ok(products); | ||
| // } | ||
| /// 나의 최근 검색어 조회 | ||
| @GetMapping("/my") | ||
| public ApiResponse<List<SearchListResponse>> getMySearches( | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ) { | ||
|
|
||
| /// 서비스 호출 | ||
| List<Search> responses = service.getMySearches(principalDetails.getId()); | ||
|
|
||
| /// 리턴 | ||
| return ApiResponse.ok(SearchListResponse.from(responses)); | ||
|
|
||
| } | ||
|
|
||
| /// 특정 검색어 삭제 | ||
| @DeleteMapping("/{id}") | ||
| public ApiResponse<Void> deleteSearch( | ||
| @PathVariable Long id, | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ) { | ||
| /// 서비스 호출 | ||
| service.deleteMySearchKeyword(id, principalDetails.getId()); | ||
|
|
||
| /// 리턴 | ||
| return ApiResponse.deleted(); | ||
| } | ||
|
|
||
|
|
||
| /// 모든 검색어 삭제 | ||
| @DeleteMapping() | ||
| public ApiResponse<Void> deleteAllSearch( | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ) { | ||
| /// 서비스 호출 | ||
| service.deleteAllKeywords(principalDetails.getId()); | ||
|
|
||
| /// 리턴 | ||
| return ApiResponse.deleted(); | ||
| } | ||
|
|
||
| /// 자동 저장 기능 조회 | ||
| @GetMapping("/auto") | ||
| public ApiResponse<String> getMyAutoSearch( | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ) { | ||
|
|
||
| /// 서비스 호출 | ||
| boolean checked = service.checkSearch(principalDetails.getId()); | ||
|
|
||
| String autoSearch = checked ? "자동 저장이 활성화되었습니다." : "자동 저장이 꺼져있습니다."; | ||
|
|
||
| /// 리턴 | ||
| return ApiResponse.ok(autoSearch); | ||
|
|
||
| } | ||
|
|
||
| /// 자동 저장 기능 켜기 | ||
| @PutMapping("/auto/on") | ||
| public ApiResponse<Void> turnOnSearch( | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ){ | ||
|
|
||
| /// 서비스 호출 | ||
| service.turnOnMySearchKeyword(principalDetails.getId()); | ||
|
|
||
| /// 리턴 | ||
| return ApiResponse.updated(); | ||
|
|
||
| } | ||
|
|
||
| /// 자동 저장 기능 끄기 | ||
| @PutMapping("/auto/off") | ||
| public ApiResponse<Void> turnOffSearch( | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ){ | ||
|
|
||
| /// 서비스 호출 | ||
| service.turnOffMySearchKeyword(principalDetails.getId()); | ||
|
|
||
| /// 리턴 | ||
| return ApiResponse.updated(); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package site.kikihi.custom.platform.adapter.in.web.dto.response.search; | ||
|
|
||
| import lombok.Builder; | ||
| import site.kikihi.custom.platform.domain.search.Search; | ||
| import java.util.List; | ||
|
|
||
| @Builder | ||
| public record SearchListResponse( | ||
| Long searchId, | ||
| String keyword | ||
| ) { | ||
|
|
||
| /// 정적 팩토리 메서드 | ||
| public static SearchListResponse from(Search search) { | ||
| return SearchListResponse.builder() | ||
| .searchId(search.getId()) | ||
| .keyword(search.getKeyword()) | ||
| .build(); | ||
| } | ||
|
|
||
| /// 정적 팩토리 메서드 | ||
| public static List<SearchListResponse> from(List<Search> searches) { | ||
| return searches.stream() | ||
| .map(SearchListResponse::from) | ||
| .toList(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||
| package site.kikihi.custom.platform.adapter.in.web.swagger; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.Operation; | ||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||||||||||||||||||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||||||||||||||||||||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||||||||||||||||||||
| import org.springframework.web.bind.annotation.RequestParam; | ||||||||||||||||||||||
| import site.kikihi.custom.global.response.ApiResponse; | ||||||||||||||||||||||
| import site.kikihi.custom.global.response.page.PageRequest; | ||||||||||||||||||||||
| import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; | ||||||||||||||||||||||
| import site.kikihi.custom.platform.adapter.in.web.dto.response.search.SearchListResponse; | ||||||||||||||||||||||
| import site.kikihi.custom.security.oauth2.domain.PrincipalDetails; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Tag(name = "검색 API", description = "검색을 위한 API 입니다.") | ||||||||||||||||||||||
| public interface SearchControllerSpec { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "검색 API", | ||||||||||||||||||||||
| description = "키워드를 바탕으로 조회합니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<List<ProductListResponse>> searchProducts( | ||||||||||||||||||||||
| @Parameter(example = "하우징") | ||||||||||||||||||||||
| @RequestParam("keyword") String keyword, | ||||||||||||||||||||||
| PageRequest pageRequest, | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "나의 최근 검색어 목록 API", | ||||||||||||||||||||||
| description = "JWT를 바탕으로 나의 최근 검색어를 조회합니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<List<SearchListResponse>> getMySearches( | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "특정 검색어 삭제 API", | ||||||||||||||||||||||
| description = "JWT를 바탕으로 특정 검색어를 삭제합니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<Void> deleteSearch( | ||||||||||||||||||||||
| @PathVariable Long id, | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "모든 검색어 삭제 API", | ||||||||||||||||||||||
| description = "JWT를 바탕으로 모든 검색어를 삭제합니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<Void> deleteAllSearch( | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "검색어 자동저장 여부 API", | ||||||||||||||||||||||
| description = "JWT를 바탕으로 자동저장을 확인합니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<String> getMyAutoSearch( | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "검색어 자동저장 기능 ON API", | ||||||||||||||||||||||
| description = "JWT를 바탕으로 자동저장을 킵니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<Void> turnOnSearch( | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
Comment on lines
+71
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오타 수정: “킵니다” → “켭니다” 사용자 노출 문구이므로 맞춤법을 맞춰 주세요. @Operation(
summary = "검색어 자동저장 기능 ON API",
- description = "JWT를 바탕으로 자동저장을 킵니다."
+ description = "JWT를 바탕으로 자동저장을 켭니다."
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Operation( | ||||||||||||||||||||||
| summary = "검색어 자동저장 기능 OFF API", | ||||||||||||||||||||||
| description = "JWT를 바탕으로 자동저장을 끕니다." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ApiResponse<Void> turnOffSearch( | ||||||||||||||||||||||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package site.kikihi.custom.platform.adapter.out; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import site.kikihi.custom.platform.adapter.out.jpa.search.SearchJpaEntity; | ||
| import site.kikihi.custom.platform.adapter.out.jpa.search.SearchJpaRepository; | ||
| import site.kikihi.custom.platform.application.out.search.SearchPort; | ||
| import site.kikihi.custom.platform.domain.search.Search; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class SearchAdapter implements SearchPort { | ||
|
|
||
| private final SearchJpaRepository repository; | ||
|
|
||
| @Override | ||
| public Search saveSearch(Search search) { | ||
|
|
||
| var entity = SearchJpaEntity.from(search); | ||
| return repository.save(entity) | ||
| .toDomain(); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<Search> getSearch(Long id) { | ||
| return repository.findById(id) | ||
| .map(SearchJpaEntity::toDomain); | ||
| } | ||
|
|
||
| @Override | ||
| public List<Search> getSearches(UUID userId) { | ||
| return repository.findByUserIdOrderByCreatedAtDesc(userId).stream() | ||
| .map(SearchJpaEntity::toDomain) | ||
| .toList(); | ||
| } | ||
|
|
||
| @Override | ||
| public void deleteSearch(Long searchId, UUID userId) { | ||
| repository.deleteByIdAndUserId(searchId, userId); | ||
| } | ||
|
Comment on lines
+41
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 권한 검증이 실질적으로 동작하지 않을 수 있습니다 — 삭제 건수 확인 로직 추가 제안
아래처럼 삭제 건수를 확인하고 0건이면 @@
+import org.springframework.dao.EmptyResultDataAccessException;
@@
@Override
public void deleteSearch(Long searchId, UUID userId) {
- repository.deleteByIdAndUserId(searchId, userId);
+ long deleted = repository.deleteByIdAndUserId(searchId, userId);
+ if (deleted == 0) {
+ throw new EmptyResultDataAccessException("No search found for given id and userId", 1);
+ }
}참고: 위 변경을 위해 |
||
|
|
||
|
|
||
| @Override | ||
| public void deleteALlSearch(UUID userId) { | ||
| repository.deleteAllByUserId(userId); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
PR/특정 브랜치에서 CI가 동작하지 않을 위험 — pull_request 트리거 추가 및 배포 단계 가드 권장
push 트리거를 develop으로 한정하면, 기능 브랜치/PR에서는 CI가 아예 실행되지 않아 품질 게이트가 비어 있게 됩니다. PR 기반 검증을 위해 pull_request 트리거를 추가하고, 배포/푸시는 push(develop)에서만 실행되도록 가드해 주세요.
아래처럼 트리거와 배포 단계 조건을 분리하는 것을 제안드립니다.
그리고 Docker 로그인/푸시 단계에 조건을 추가해 PR 이벤트에서는 스킵되도록 합니다.
📝 Committable suggestion
🤖 Prompt for AI Agents