From 1246bab1105f81dd6353e0fc576234b29287307e Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 17:07:56 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=93=82=20file/KIKI-61=20:=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/SearchController.java | 27 ++++++++----------- .../in/web/swagger/SearchControllerSpec.java | 24 +++++++++++++++++ .../SearchUseCase.java} | 4 +-- ...cSearchService.java => SearchService.java} | 11 ++------ 4 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java rename src/main/java/site/kikihi/custom/platform/application/in/{product/ProductSearchUseCase.java => search/SearchUseCase.java} (74%) rename src/main/java/site/kikihi/custom/platform/application/service/{ElasticSearchService.java => SearchService.java} (90%) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java index 5ca63ab..73d6cfc 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java @@ -1,8 +1,10 @@ package site.kikihi.custom.platform.adapter.in.web; 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.swagger.SearchControllerSpec; +import site.kikihi.custom.platform.application.service.SearchService; import site.kikihi.custom.platform.domain.product.Product; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -12,33 +14,26 @@ @RestController @RequestMapping("/api/v1/search") @RequiredArgsConstructor -public class SearchController { +public class SearchController implements SearchControllerSpec { - private final ElasticSearchService searchService; + private final SearchService searchService; // 상품 검색 @GetMapping public ApiResponse> searchProducts( @RequestParam("keyword") String keyword, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + PageRequest pageRequest, @RequestParam(defaultValue = "0.001") float minScore ) { - List productList = searchService.searchProducts(keyword, page, size, minScore); + /// 서비스 호출 + List productList = searchService.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), minScore); + + /// DTO 수정 List responses = ProductListResponse.from(productList); + /// 응답 return ApiResponse.ok(responses); } - // 상품 필터링 -// @GetMapping("/filter") -// public ApiResponse> filterProducts( -// @RequestParam("keyword") String keyword, -// @RequestBody SearchRequest req, -// PageRequest pageRequest -// ) { -// List products = searchService.filterProducts(keyword, req.manufacturer(), req.minPrice(), req.maxPrice(), pageRequest.getPage(), pageRequest.getSize()); -// return ApiResponse.ok(products); -// } } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java new file mode 100644 index 0000000..87d4f5d --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java @@ -0,0 +1,24 @@ +package site.kikihi.custom.platform.adapter.in.web.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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 java.util.List; + +@Tag(name = "검색 API", description = "검색을 위한 API 입니다.") +public interface SearchControllerSpec { + + @Operation( + summary = "검색 API", + description = "키워드를 바탕으로 조회합니다." + ) + ApiResponse> searchProducts( + @RequestParam("keyword") String keyword, + PageRequest pageRequest, + @RequestParam(defaultValue = "0.001") float minScore + ); +} diff --git a/src/main/java/site/kikihi/custom/platform/application/in/product/ProductSearchUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java similarity index 74% rename from src/main/java/site/kikihi/custom/platform/application/in/product/ProductSearchUseCase.java rename to src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java index b55ca3a..3a72abf 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/product/ProductSearchUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java @@ -1,9 +1,9 @@ -package site.kikihi.custom.platform.application.in.product; +package site.kikihi.custom.platform.application.in.search; import site.kikihi.custom.platform.domain.product.Product; import java.util.List; -public interface ProductSearchUseCase { +public interface SearchUseCase { List searchProducts(String keyword, int page, int size, float minScore); List filterProducts(String keyword, String manufacturer, Double minPrice, Double maxPrice, int page, int size); } diff --git a/src/main/java/site/kikihi/custom/platform/application/service/ElasticSearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java similarity index 90% rename from src/main/java/site/kikihi/custom/platform/application/service/ElasticSearchService.java rename to src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index 442d4a8..fbd9bff 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/ElasticSearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -2,7 +2,7 @@ import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESDocument; import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESRepository; -import site.kikihi.custom.platform.application.in.product.ProductSearchUseCase; +import site.kikihi.custom.platform.application.in.search.SearchUseCase; import site.kikihi.custom.platform.domain.product.Product; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -20,18 +20,11 @@ @Service @RequiredArgsConstructor -public class ElasticSearchService implements ProductSearchUseCase { +public class SearchService implements SearchUseCase { private final ElasticsearchOperations elasticsearchOperations; private final ProductESRepository productESRepository; -// // 상품 저장 -// public Product saveProduct(Product product) { -// ProductESDocument doc = ProductESDocument.toESDocument(product); -// ProductESDocument saved = productESRepository.save(doc); -// return toDomain(saved); -// } - // 키워드 검색 (name, description) @Override public List searchProducts(String keyword, int page, int size, float minScore) { From b02957173941d41230349a91301bd7f6ab792f46 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 17:08:58 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor/KIKI-61=20:?= =?UTF-8?q?=20CICD=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index c30caa8..af726fe 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -9,7 +9,7 @@ name: 키키하이 dev CI-CD 파이프라인 on: push: - branches: [ "develop","feat/product/KIKI-61-ElasticSearch"] + branches: [ "develop"] jobs: #1. 개발 서버 CI, Build 용 From e680d73dbd6b0c8f2d5e19700fcdb6a8607a27c2 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 17:38:31 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/jpa/search/SearchJpaEntity.java | 48 +++++++++++++++++++ .../out/jpa/search/SearchJpaRepository.java | 6 +++ .../custom/platform/domain/search/Search.java | 32 +++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java create mode 100644 src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java create mode 100644 src/main/java/site/kikihi/custom/platform/domain/search/Search.java diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java new file mode 100644 index 0000000..16997f3 --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java @@ -0,0 +1,48 @@ +package site.kikihi.custom.platform.adapter.out.jpa.search; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.kikihi.custom.platform.adapter.out.jpa.BaseTimeEntity; +import site.kikihi.custom.platform.domain.search.Search; + +import java.util.UUID; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +public class SearchJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private UUID userId; + + private String keyword; + + /// 정적 팩토리 메서드 + public static SearchJpaEntity from(Search search) { + return SearchJpaEntity.builder() + .userId(search.getUserId()) + .keyword(search.getKeyword()) + .build(); + } + + /// 도메인 + public Search toDomain() { + return Search.builder() + .id(id) + .keyword(keyword) + .userId(userId) + .build(); + + } +} diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java new file mode 100644 index 0000000..eedfbfb --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java @@ -0,0 +1,6 @@ +package site.kikihi.custom.platform.adapter.out.jpa.search; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SearchJpaRepository extends JpaRepository { +} diff --git a/src/main/java/site/kikihi/custom/platform/domain/search/Search.java b/src/main/java/site/kikihi/custom/platform/domain/search/Search.java new file mode 100644 index 0000000..98c488c --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/domain/search/Search.java @@ -0,0 +1,32 @@ +package site.kikihi.custom.platform.domain.search; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.kikihi.custom.platform.domain.BaseDomain; + +import java.util.UUID; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +public class Search extends BaseDomain { + + private Long id; + + private UUID userId; + + private String keyword; + + + /// 정적 팩토리 메서드 + public static Search of(UUID userId, String keyword) { + return Search.builder() + .userId(userId) + .keyword(keyword) + .build(); + } + +} From 7a8bc2ac78bf34cb344881da2e18e982a71e3d88 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 21:38:16 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/SearchController.java | 85 +++++++++- .../response/search/SearchListResponse.java | 27 +++ .../in/web/swagger/SearchControllerSpec.java | 57 ++++++- .../platform/adapter/out/SearchAdapter.java | 51 ++++++ .../out/jpa/search/SearchJpaRepository.java | 9 + .../application/in/search/SearchUseCase.java | 32 +++- .../application/out/search/SearchPort.java | 24 +++ .../application/service/SearchService.java | 160 +++++++++++++----- 8 files changed, 398 insertions(+), 47 deletions(-) create mode 100644 src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java create mode 100644 src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java create mode 100644 src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java index 73d6cfc..72def93 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java @@ -1,33 +1,41 @@ 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.adapter.in.web.dto.response.search.SearchListResponse; import site.kikihi.custom.platform.adapter.in.web.swagger.SearchControllerSpec; import site.kikihi.custom.platform.application.service.SearchService; 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 implements SearchControllerSpec { - private final SearchService searchService; + private final SearchService service; - // 상품 검색 + /// 상품 검색 @GetMapping public ApiResponse> searchProducts( @RequestParam("keyword") String keyword, PageRequest pageRequest, - @RequestParam(defaultValue = "0.001") float minScore + @AuthenticationPrincipal PrincipalDetails principalDetails ) { + /// 유저가 없다면 null 저장 + UUID userId = principalDetails != null ? principalDetails.getId() : null; + /// 서비스 호출 - List productList = searchService.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), minScore); + List productList = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId); /// DTO 수정 List responses = ProductListResponse.from(productList); @@ -36,4 +44,73 @@ public ApiResponse> searchProducts( return ApiResponse.ok(responses); } + /// 나의 최근 검색어 조회 + @GetMapping("/my") + public ApiResponse> getMySearches( + @AuthenticationPrincipal PrincipalDetails principalDetails + ) { + + /// 서비스 호출 + List responses = service.getMySearches(principalDetails.getId()); + + /// 리턴 + return ApiResponse.ok(SearchListResponse.from(responses)); + + } + + + /// 특정 검색어 삭제 + @DeleteMapping("/{id}") + public ApiResponse deleteSearch( + @PathVariable Long id, + @AuthenticationPrincipal PrincipalDetails principalDetails + ) { + /// 서비스 호출 + service.deleteMySearchKeyword(id, principalDetails.getId()); + + + /// 리턴 + return ApiResponse.deleted(); + } + + + /// 모든 검색어 삭제 + @DeleteMapping() + public ApiResponse deleteAllSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ) { + /// 서비스 호출 + service.deleteAllKeywords(principalDetails.getId()); + + /// 리턴 + return ApiResponse.deleted(); + } + + /// 자동 저장 기능 켜기 + @PutMapping("/auto/on") + public ApiResponse turnOnSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ){ + + /// 서비스 호출 + service.turnOnMySearchKeyword(principalDetails.getId()); + + /// 리턴 + return ApiResponse.updated(); + + } + + /// 자동 저장 기능 끄기 + @PutMapping("/auto/off") + public ApiResponse turnOffSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ){ + + /// 서비스 호출 + service.turnOffMySearchKeyword(principalDetails.getId()); + + /// 리턴 + return ApiResponse.updated(); + } + } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java new file mode 100644 index 0000000..07a96c4 --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java @@ -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 from(List searches) { + return searches.stream() + .map(SearchListResponse::from) + .toList(); + } +} diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java index 87d4f5d..09721b3 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java @@ -2,10 +2,14 @@ import io.swagger.v3.oas.annotations.Operation; 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; @@ -19,6 +23,57 @@ public interface SearchControllerSpec { ApiResponse> searchProducts( @RequestParam("keyword") String keyword, PageRequest pageRequest, - @RequestParam(defaultValue = "0.001") float minScore + @AuthenticationPrincipal PrincipalDetails principalDetails + ); + + + @Operation( + summary = "나의 최근 검색어 목록 API", + description = "JWT를 바탕으로 나의 최근 검색어를 조회합니다." + ) + ApiResponse> getMySearches( + @AuthenticationPrincipal PrincipalDetails principalDetails + ); + + @Operation( + summary = "특정 검색어 삭제 API", + description = "JWT를 바탕으로 특정 검색어를 삭제합니다." + ) + ApiResponse deleteSearch( + @PathVariable Long id, + @AuthenticationPrincipal PrincipalDetails principalDetails + ); + + + + + @Operation( + summary = "모든 검색어 삭제 API", + description = "JWT를 바탕으로 모든 검색어를 삭제합니다." + ) + ApiResponse deleteAllSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ); + + + + + @Operation( + summary = "검색어 자동저장 기능 ON API", + description = "JWT를 바탕으로 자동저장을 킵니다." + ) + ApiResponse turnOnSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ); + + + + + @Operation( + summary = "검색어 자동저장 기능 OFF API", + description = "JWT를 바탕으로 자동저장을 끕니다." + ) + ApiResponse turnOffSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails ); } diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java b/src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java new file mode 100644 index 0000000..2e74b27 --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java @@ -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 getSearch(Long id) { + return repository.findById(id) + .map(SearchJpaEntity::toDomain); + } + + @Override + public List getSearches(UUID userId) { + return repository.findByUserIdOrderByCreatedAtDesc(userId).stream() + .map(SearchJpaEntity::toDomain) + .toList(); + } + + @Override + public void deleteSearch(Long searchId, UUID userId) { + repository.deleteByIdAndUserId(searchId, userId); + } + + + @Override + public void deleteALlSearch(UUID userId) { + repository.deleteAllByUserId(userId); + } +} diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java index eedfbfb..368aeb1 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java @@ -2,5 +2,14 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.UUID; + public interface SearchJpaRepository extends JpaRepository { + + List findByUserIdOrderByCreatedAtDesc(UUID userId); + + void deleteAllByUserId(UUID userId); + + void deleteByIdAndUserId(Long id, UUID userId); } diff --git a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java index 3a72abf..64401f6 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java @@ -1,9 +1,37 @@ package site.kikihi.custom.platform.application.in.search; import site.kikihi.custom.platform.domain.product.Product; +import site.kikihi.custom.platform.domain.search.Search; + import java.util.List; +import java.util.UUID; +/** + * 검색을 위한 유즈케이스입니다 + * - 키워드 기반 검색 + * - 나의 최근 검색어 조회 + * - 나의 최근 검색어 삭제 + * - 나의 최근 검색어 저장 끄기 + */ public interface SearchUseCase { - List searchProducts(String keyword, int page, int size, float minScore); - List filterProducts(String keyword, String manufacturer, Double minPrice, Double maxPrice, int page, int size); + + /// 검색 + List searchProducts(String keyword, int page, int size, UUID userId); + + /// 나의 검색에 목록 확인하기 + List getMySearches(UUID userId); + + /// 키워드 하나 삭제하기 + void deleteMySearchKeyword(Long searchId, UUID userId); + + /// 키워드 모두 삭제하기 + void deleteAllKeywords(UUID userId); + + /// 나의 최근 검색어 저장 끄기 + void turnOffMySearchKeyword(UUID userId); + + /// 나의 최근 검색어 저장 켜기 + void turnOnMySearchKeyword(UUID userId); + + } diff --git a/src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java b/src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java new file mode 100644 index 0000000..6faee88 --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java @@ -0,0 +1,24 @@ +package site.kikihi.custom.platform.application.out.search; + +import site.kikihi.custom.platform.domain.search.Search; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface SearchPort { + + /// 최근 검색어 저장 + Search saveSearch(Search search); + + Optional getSearch(Long id); + + /// 유저의 최근 검색어 보기 + List getSearches(UUID userId); + + /// 최근 검색어 하나 삭제하기 + void deleteSearch(Long searchId, UUID userId); + + /// 최근 검색어 모두 삭제하기 + void deleteALlSearch(UUID userId); + +} diff --git a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index fbd9bff..ed50b6d 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -1,8 +1,11 @@ package site.kikihi.custom.platform.application.service; +import site.kikihi.custom.global.response.ErrorCode; import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESDocument; import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESRepository; import site.kikihi.custom.platform.application.in.search.SearchUseCase; +import site.kikihi.custom.platform.application.out.search.SearchPort; +import site.kikihi.custom.platform.application.out.user.UserPort; import site.kikihi.custom.platform.domain.product.Product; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -13,21 +16,32 @@ import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import site.kikihi.custom.platform.domain.search.Search; +import site.kikihi.custom.platform.domain.user.User; -import java.util.ArrayList; import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class SearchService implements SearchUseCase { + /// 의존성 private final ElasticsearchOperations elasticsearchOperations; private final ProductESRepository productESRepository; + private final SearchPort port; - // 키워드 검색 (name, description) + /// 외부 의존성 + private final UserPort userPort; + + /// 스태틱 + private final Float minScore = 0.001f; + + /// 키워드 검색 (name, description) @Override - public List searchProducts(String keyword, int page, int size, float minScore) { + public List searchProducts(String keyword, int page, int size, UUID userId) { // match 쿼리 구성 Query nameMatch = MatchQuery.of(m -> m.field("name").query(keyword))._toQuery(); Query descMatch = MatchQuery.of(m -> m.field("description").query(keyword))._toQuery(); @@ -46,6 +60,17 @@ public List searchProducts(String keyword, int page, int size, float mi .withMinScore(minScore) // <<-- 추가됨 .build(); + /// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기 + if (userId != null) { + + /// 유저 조회 + User user = getUser(userId); + + /// DB에 최신 검색어 저장하기 + Search search = Search.of(user.getId(), keyword); + port.saveSearch(search); + } + return elasticsearchOperations.search(query, ProductESDocument.class) .stream() .map(SearchHit::getContent) @@ -53,49 +78,104 @@ public List searchProducts(String keyword, int page, int size, float mi .collect(Collectors.toList()); } + @Override + public List getMySearches(UUID userId) { + + /// 유저 + User user = getUser(userId); + + /// 유저의 최근 검색어 조회하기 + return port.getSearches(user.getId()); + } - // 필터링 + /** + * 특정 키워드를 검색 기록에서 삭제합니다. + * @param searchId 삭제할 키워드 + * @param userId 유저 ID + */ @Override - public List filterProducts(String keyword, String manufacturer, Double minPrice, Double maxPrice, int page, int size) { - // 1. should 쿼리(키워드 검색) - List shouldQueries = new ArrayList<>(); - shouldQueries.add(MatchQuery.of(m -> m.field("productName").query(keyword))._toQuery()); - shouldQueries.add(MatchQuery.of(m -> m.field("description").query(keyword))._toQuery()); - - // 2. must 쿼리(제조사, 가격) - List mustQueries = new ArrayList<>(); - if (manufacturer != null && !manufacturer.isEmpty()) { - mustQueries.add(MatchQuery.of(m -> m.field("manufacturer").query(manufacturer))._toQuery()); + public void deleteMySearchKeyword(Long searchId, UUID userId) { + + /// 유저 예외 처리 + User user = getUser(userId); + + /// 검색 기록 예외 처리 + Search search = getSearch(searchId); + + /// 유저와 특정 키워드를 바탕으로 삭제합니다. + try { + port.deleteSearch(search.getId(), user.getId()); + } catch (Exception e) { + throw new IllegalArgumentException(ErrorCode.UNAUTHORIZED_DELETE_SEARCH.getMessage()); } -// if (minPrice != null || maxPrice != null) { -// RangeQuery rangeQuery = RangeQuery.of(r -> r -// .field("price") -// .gte(minPrice != null ? JsonData.of(minPrice) : null) -// .lte(maxPrice != null ? JsonData.of(maxPrice) : null) -// ); -// mustQueries.add(rangeQuery._toQuery()); -// } - - // 3. BoolQuery 조립 - Query boolQuery = BoolQuery.of(b -> b - .should(shouldQueries) - .must(mustQueries) - .minimumShouldMatch("1") - )._toQuery(); - // 4. NativeQuery 생성 - NativeQuery nativeQuery = NativeQuery.builder() - .withQuery(boolQuery) - .withPageable(PageRequest.of(page, size)) - .build(); + } + + /** + * 모든 키워드를 검색 기록에서 삭제합니다. + * @param userId 유저ID + */ + @Override + public void deleteAllKeywords(UUID userId) { + + /// 유저 예외 처리 + User user = getUser(userId); + + /// 전부 삭제하기 + port.deleteALlSearch(user.getId()); + } + + + /** + * 검색 기록을 저장하지않도록 끕니다. + * @param userId 유저 ID + */ + @Override + public void turnOffMySearchKeyword(UUID userId) { + + /// 유저 + User user = getUser(userId); + + /// 이미 켜져있다면 + if (!user.isSearch()) { + throw new IllegalStateException(ErrorCode.ALREADY_ON.getMessage()); + } + + /// 업데이트 + userPort.updateUser(user); + } + + @Override + public void turnOnMySearchKeyword(UUID userId) { + /// 유저 + User user = getUser(userId); + + /// 이미 켜져있다면 + if (user.isSearch()) { + throw new IllegalStateException(ErrorCode.ALREADY_ON.getMessage()); + } + + /// 비즈니스 로직 수행 + user.turnOnSearch(); + + /// 업데이트 + userPort.updateUser(user); - // 5. 검색 및 변환 - return elasticsearchOperations.search(nativeQuery, ProductESDocument.class) - .stream() - .map(SearchHit::getContent) - .map(ProductESDocument::toDomain) - .collect(Collectors.toList()); } + /// 유저 조회 + private User getUser(UUID userId) { + + return userPort.loadUserById(userId) + .orElseThrow(() -> new NoSuchElementException(ErrorCode.USER_NOT_FOUND.getMessage())); + } + + + /// 검색 기록 조회 + private Search getSearch(Long searchId) { + + return port.getSearch(searchId) + .orElseThrow(() -> new NoSuchElementException(ErrorCode.SEARCH_NOT_FOUND.getMessage())); + } } From aec34e7268dafb3b3dcd09ec20297b488bdd0658 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 21:38:57 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/out/jpa/user/UserJpaEntity.java | 4 ++++ .../platform/application/service/SearchService.java | 3 +++ .../kikihi/custom/platform/domain/user/User.java | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java index 535744b..46b5861 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java @@ -41,6 +41,8 @@ public class UserJpaEntity extends BaseTimeEntity { @Embedded private AddressJpaEntity address; + private boolean isSearch; + @PrePersist public void generateUUID() { if (this.id == null) { @@ -59,6 +61,7 @@ public static UserJpaEntity from(User user) { .role(user.getRole()) .profileImage(user.getProfileImage()) .address(AddressJpaEntity.from(user.getAddress())) + .isSearch(user.isSearch()) .build(); } @@ -73,6 +76,7 @@ public User toDomain() { .role(role) .profileImage(profileImage) .address(address.toDomain()) + .isSearch(isSearch) .build(); } diff --git a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index ed50b6d..515113a 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -141,6 +141,9 @@ public void turnOffMySearchKeyword(UUID userId) { throw new IllegalStateException(ErrorCode.ALREADY_ON.getMessage()); } + /// 비즈니스 로직 수행 + user.turnOffSearch(); + /// 업데이트 userPort.updateUser(user); } diff --git a/src/main/java/site/kikihi/custom/platform/domain/user/User.java b/src/main/java/site/kikihi/custom/platform/domain/user/User.java index 3a17baa..6873967 100644 --- a/src/main/java/site/kikihi/custom/platform/domain/user/User.java +++ b/src/main/java/site/kikihi/custom/platform/domain/user/User.java @@ -24,7 +24,9 @@ public class User { private Role role; private String profileImage; private Address address; + private boolean isSearch = true; + /// 정적 팩토리 메서드 public static User of(OAuth2UserInfo userInfo) { return User.builder() .id(UUID.randomUUID()) @@ -36,6 +38,17 @@ public static User of(OAuth2UserInfo userInfo) { .profileImage(userInfo.getImageUrl()) .role(Role.USER) .address(Address.of()) + .isSearch(true) .build(); } + + /// 비즈니스 로직 + public void turnOnSearch() { + isSearch = true; + } + + public void turnOffSearch() { + isSearch = false; + } + } From 8e0b4252a1cb915b93ccda6bb91c76820d37af96 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 21:39:17 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EA=B4=80=EB=A0=A8=20JWT=20=EB=B0=8F=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=BD=94=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kikihi/custom/global/response/ErrorCode.java | 16 ++++++++++------ .../jwt/filter/JwtAuthenticationFilter.java | 5 +++++ .../jwt/filter/RequestMatcherHolder.java | 9 ++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/site/kikihi/custom/global/response/ErrorCode.java b/src/main/java/site/kikihi/custom/global/response/ErrorCode.java index 98376e1..5984e38 100644 --- a/src/main/java/site/kikihi/custom/global/response/ErrorCode.java +++ b/src/main/java/site/kikihi/custom/global/response/ErrorCode.java @@ -7,7 +7,7 @@ /** * 애플리케이션 전역에서 사용하는 에러 코드 Enum입니다. * 각 에러는 고유 코드, HTTP 상태, 메시지를 포함합니다. - * + *

* 400 : 잘못된 요청 에러 * 401 : 로그인 관련 에러 * 403 : 권한 부족 관련 에러 @@ -28,6 +28,8 @@ public enum ErrorCode { INVALID_INPUT(400_002, HttpStatus.BAD_REQUEST, "입력값이 올바르지 않습니다."), NULL_VALUE(400_003, HttpStatus.BAD_REQUEST, "Null 값이 들어왔습니다."), TEST_ERROR(400_004, HttpStatus.BAD_REQUEST, "테스트 에러입니다."), + ALREADY_ON(400_005, HttpStatus.BAD_REQUEST, "이미 검색어 저장 기능이 켜져있습니다"), + ALREADY_OFF(400_006, HttpStatus.BAD_REQUEST, "이미 검색어 저장 기능이 꺼져있습니다"), // ======================== @@ -48,7 +50,6 @@ public enum ErrorCode { TOKEN_NOT_FOUND_COOKIE(401_011, HttpStatus.UNAUTHORIZED, "쿠키에 리프레시 토큰이 존재하지 않습니다."), - // ======================== // 403 Forbidden // ======================== @@ -57,6 +58,7 @@ public enum ErrorCode { UNAUTHORIZED_POST_ACCESS(403_002, HttpStatus.FORBIDDEN, "해당 게시글에 접근할 권한이 없습니다."), UNAUTHORIZED_DELETE_BOOKMARK(403_003, HttpStatus.FORBIDDEN, "해당 북마크를 삭제할 권한이 없습니다."), UNAUTHORIZED_DELETE_CUSTOM(403_004, HttpStatus.FORBIDDEN, "해당 커스텀을 삭제할 권한이 없습니다."), + UNAUTHORIZED_DELETE_SEARCH(403_005, HttpStatus.FORBIDDEN, "검색기록을 삭제할 권한이 없습니다."), // ======================== @@ -69,20 +71,22 @@ public enum ErrorCode { POST_TYPE_NOT_FOUND(404_004, HttpStatus.NOT_FOUND, "게시글 타입을 찾을 수 없습니다."), COMMENT_NOT_FOUND(404_005, HttpStatus.NOT_FOUND, "요청한 댓글을 찾을 수 없습니다."), PRODUCT_NOT_FOUND(404_006, HttpStatus.NOT_FOUND, "요청한 상품을 찾을 수 없습니다."), - BOOKMARK_NOT_FOUND(404_007,HttpStatus.NOT_FOUND,"요청한 북마크를 찾을 수 없습니다."), - CUSTOM_NOT_FOUND(404_008,HttpStatus.NOT_FOUND,"요청한 커스텀 키보드를 찾을 수 없습니다."), + BOOKMARK_NOT_FOUND(404_007, HttpStatus.NOT_FOUND, "요청한 북마크를 찾을 수 없습니다."), + CUSTOM_NOT_FOUND(404_008, HttpStatus.NOT_FOUND, "요청한 커스텀 키보드를 찾을 수 없습니다."), + SEARCH_NOT_FOUND(404_009, HttpStatus.NOT_FOUND, "요청한 검색기록을 찾을 수 없습니다."), // ======================== // 409 Conflict // ======================== DUPLICATE_EMAIL(409_001, HttpStatus.CONFLICT, "이미 사용 중인 이메일입니다."), - BOOKMARK_ALREADY(409_003,HttpStatus.CONFLICT,"이미 해당 상품에 북마크를 등록했습니다"), + BOOKMARK_ALREADY(409_003, HttpStatus.CONFLICT, "이미 해당 상품에 북마크를 등록했습니다"), // ======================== // 500 Internal Server Error // ======================== - INTERNAL_SERVER_ERROR(500_000, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."); + INTERNAL_SERVER_ERROR(500_000, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), + ; // 기타 공통 diff --git a/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java index e6062ce..4d43a23 100644 --- a/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java @@ -115,6 +115,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) { return false; } + /// 검색은 회원/비회원 구분해야되기에 필터를 타도록 설정 + if (request.getRequestURI().startsWith("/api/v1/search")) { + return false; + } + /// null 인 것 해결 return requestMatcherHolder.getRequestMatchersByMinRole(null) .matches(request); diff --git a/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java b/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java index ff3c7b0..c2743f5 100644 --- a/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java +++ b/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java @@ -7,6 +7,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.stereotype.Component; +import site.kikihi.custom.platform.domain.user.User; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -34,9 +35,15 @@ public class RequestMatcherHolder { // 상품 관련 new RequestInfo(GET, "/api/v1/products/**", null), - //추천 관련 + // 추천 관련 new RequestInfo(GET, "/api/v1/recommend/**", null), + // 검색 관련 + new RequestInfo(GET, "/api/v1/search", null), + new RequestInfo(GET, "/api/v1/search/my", Role.USER), + new RequestInfo(DELETE, "/api/v1/search/**", Role.USER), + new RequestInfo(PUT, "/api/v1/search/**", Role.USER), + // static resources new RequestInfo(GET, "/docs/**", null), new RequestInfo(GET, "/*.ico", null), From 0da16c4ecd3772ed5a2746771461f2c8a792b26f Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 23:06:36 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B4=80=EB=A0=A8=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/platform/adapter/out/UserAdapter.java | 13 +++++++++++-- .../adapter/out/jpa/user/UserJpaEntity.java | 4 ++++ .../platform/application/out/user/UserPort.java | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java b/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java index 56412e6..a1c4103 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java @@ -1,5 +1,7 @@ package site.kikihi.custom.platform.adapter.out; +import org.springframework.transaction.annotation.Transactional; +import site.kikihi.custom.global.response.ErrorCode; import site.kikihi.custom.platform.adapter.out.jpa.user.UserJpaEntity; import site.kikihi.custom.platform.adapter.out.jpa.user.UserJpaRepository; import site.kikihi.custom.platform.application.out.user.UserPort; @@ -25,8 +27,15 @@ public User saveUser(User user) { } @Override - public User updateUser(User user) { - return null; + @Transactional + public void updateUser(User user) { + + /// 조회 + var entity = userJpaRepository.findById(user.getId()) + .orElseThrow(() -> new IllegalArgumentException(ErrorCode.USER_NOT_FOUND.getMessage())); + + /// 자동저장 여부 수정 + entity.updateSearch(user.isSearch()); } @Override diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java index 46b5861..8a0a235 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java @@ -80,4 +80,8 @@ public User toDomain() { .build(); } + /// 엔티티 수정용 + public void updateSearch(boolean isSearch) { + this.isSearch = isSearch; + } } diff --git a/src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java b/src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java index 7191910..9f39ba0 100644 --- a/src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java +++ b/src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java @@ -12,7 +12,7 @@ public interface UserPort { User saveUser(User user); // 수정하기 - User updateUser(User user); + void updateUser(User user); /// 조회하기 boolean checkExistingById(UUID userId); From c7659e9d396b8a677dfa94b22db5b88f16154cbc Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 23:11:37 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=A0=80=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84=20-=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=A0=80=EC=9E=A5=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=8C=8C=EC=95=85=20-=20=EC=B7=A8=EC=86=8C/=ED=99=95=EC=A0=95?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/SearchController.java | 22 +++++++-- .../in/web/swagger/SearchControllerSpec.java | 9 ++++ .../application/in/search/SearchUseCase.java | 3 ++ .../application/service/SearchService.java | 46 ++++++++++--------- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java index 72def93..f8785ab 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java @@ -6,7 +6,7 @@ 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.platform.adapter.in.web.swagger.SearchControllerSpec; -import site.kikihi.custom.platform.application.service.SearchService; +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.*; @@ -21,7 +21,7 @@ @RequiredArgsConstructor public class SearchController implements SearchControllerSpec { - private final SearchService service; + private final SearchUseCase service; /// 상품 검색 @GetMapping @@ -58,7 +58,6 @@ public ApiResponse> getMySearches( } - /// 특정 검색어 삭제 @DeleteMapping("/{id}") public ApiResponse deleteSearch( @@ -68,7 +67,6 @@ public ApiResponse deleteSearch( /// 서비스 호출 service.deleteMySearchKeyword(id, principalDetails.getId()); - /// 리턴 return ApiResponse.deleted(); } @@ -86,6 +84,22 @@ public ApiResponse deleteAllSearch( return ApiResponse.deleted(); } + /// 자동 저장 기능 조회 + @GetMapping("/auto") + public ApiResponse getMyAutoSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ) { + + /// 서비스 호출 + boolean checked = service.checkSearch(principalDetails.getId()); + + String autoSearch = checked ? "자동 저장이 활성화되었습니다." : "자동 저장이 꺼져있습니다."; + + /// 리턴 + return ApiResponse.ok(autoSearch); + + } + /// 자동 저장 기능 켜기 @PutMapping("/auto/on") public ApiResponse turnOnSearch( diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java index 09721b3..6794a94 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java @@ -1,6 +1,7 @@ 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; @@ -21,6 +22,7 @@ public interface SearchControllerSpec { description = "키워드를 바탕으로 조회합니다." ) ApiResponse> searchProducts( + @Parameter(example = "하우징") @RequestParam("keyword") String keyword, PageRequest pageRequest, @AuthenticationPrincipal PrincipalDetails principalDetails @@ -56,6 +58,13 @@ ApiResponse deleteAllSearch( ); + @Operation( + summary = "검색어 자동저장 여부 API", + description = "JWT를 바탕으로 자동저장을 확인합니다." + ) + ApiResponse getMyAutoSearch( + @AuthenticationPrincipal PrincipalDetails principalDetails + ); @Operation( diff --git a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java index 64401f6..ab6d812 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java @@ -27,6 +27,9 @@ public interface SearchUseCase { /// 키워드 모두 삭제하기 void deleteAllKeywords(UUID userId); + /// 나의 최근 검색어 여부 + boolean checkSearch(UUID userId); + /// 나의 최근 검색어 저장 끄기 void turnOffMySearchKeyword(UUID userId); diff --git a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java index 515113a..24a3dfc 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/SearchService.java @@ -66,9 +66,13 @@ public List searchProducts(String keyword, int page, int size, UUID use /// 유저 조회 User user = getUser(userId); - /// DB에 최신 검색어 저장하기 - Search search = Search.of(user.getId(), keyword); - port.saveSearch(search); + /// 자동저장이 ON인 유저만 저장한다. + if (user.isSearch()) { + + /// DB에 최신 검색어 저장하기 + Search search = Search.of(user.getId(), keyword); + port.saveSearch(search); + } } return elasticsearchOperations.search(query, ProductESDocument.class) @@ -125,6 +129,16 @@ public void deleteAllKeywords(UUID userId) { port.deleteALlSearch(user.getId()); } + @Override + public boolean checkSearch(UUID userId) { + + /// 유저 예외 처리 + User user = getUser(userId); + + /// 유저의 여부 체크 + return user.isSearch(); + } + /** * 검색 기록을 저장하지않도록 끕니다. @@ -136,16 +150,11 @@ public void turnOffMySearchKeyword(UUID userId) { /// 유저 User user = getUser(userId); - /// 이미 켜져있다면 - if (!user.isSearch()) { - throw new IllegalStateException(ErrorCode.ALREADY_ON.getMessage()); + /// 켜져있을때만 끌 수있게 + if (user.isSearch()) { + user.turnOffSearch(); + userPort.updateUser(user); } - - /// 비즈니스 로직 수행 - user.turnOffSearch(); - - /// 업데이트 - userPort.updateUser(user); } @Override @@ -153,17 +162,12 @@ public void turnOnMySearchKeyword(UUID userId) { /// 유저 User user = getUser(userId); - /// 이미 켜져있다면 - if (user.isSearch()) { - throw new IllegalStateException(ErrorCode.ALREADY_ON.getMessage()); + /// 꺼져있을때만 켤 수있게 + if (!user.isSearch()) { + user.turnOnSearch(); + userPort.updateUser(user); } - /// 비즈니스 로직 수행 - user.turnOnSearch(); - - /// 업데이트 - userPort.updateUser(user); - } /// 유저 조회 From 3501e997872344125f8dbd0904cb4dc1471d6ef8 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 23:11:46 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=9E=90=20=ED=86=A0=ED=81=B0=EC=9A=A9=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kikihi/custom/platform/adapter/in/web/DevAuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java index cb91a59..96111e9 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java @@ -74,6 +74,7 @@ private User createDev() { .provider(Provider.KAKAO) // 테스트용 값 (Enum) .role(Role.ADMIN) // 관리자 권한 부여 .address(Address.of()) + .isSearch(true) .build(); } From 3797aabab641be10df3adc15b34c772dfe626538 Mon Sep 17 00:00:00 2001 From: eedo_y Date: Thu, 21 Aug 2025 23:17:14 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=E2=9C=A8=20feat/KIKI-61=20:=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EA=B4=80=EB=A0=A8=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kikihi/custom/security/jwt/filter/RequestMatcherHolder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java b/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java index c2743f5..355b71d 100644 --- a/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java +++ b/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java @@ -40,7 +40,7 @@ public class RequestMatcherHolder { // 검색 관련 new RequestInfo(GET, "/api/v1/search", null), - new RequestInfo(GET, "/api/v1/search/my", Role.USER), + new RequestInfo(GET, "/api/v1/search/**", Role.USER), new RequestInfo(DELETE, "/api/v1/search/**", Role.USER), new RequestInfo(PUT, "/api/v1/search/**", Role.USER),