Skip to content

Conversation

@doup2001
Copy link
Member

@doup2001 doup2001 commented Aug 21, 2025

📌 작업한 내용

  • 검색 기능 관련 도메인 설정 및 검색 로직 구현
  • 기존 검색 로직에 최근검색어 저장하도록 수정
  • 자동저장 기능 지원을 위해 유저 도메인 수정 및 관련 기능 추가
  • 권한 검증 및 유저 정보 체크 로직 강화
  • 검색관련 JWT 필터 및 에러 코드 처리 추가
  • 개발자 토큰 대응을 위한 유저 엔티티 수정
  • 검색 클래스 이름 및 구조 리팩토링

🔍 참고 사항

x

🖼️ 스크린샷

  • 나의 검색어 목록
스크린샷 2025-08-21 23 00 00
  • 자동저장 활성화
스크린샷 2025-08-21 23 10 38
  • 자동저장 끄기
스크린샷 2025-08-21 23 10 43

🔗 관련 이슈

#64

✅ 체크리스트

  • 로컬에서 빌드 및 테스트 완료
  • 코드 리뷰 반영 완료
  • 문서화 필요 여부 확인

Summary by CodeRabbit

  • New Features
    • 최근 검색어 조회/개별 삭제/전체 삭제 기능 추가(로그인 필요)
    • 검색어 자동 저장 상태 확인 및 켜기/끄기 토글 지원
    • 로그인 시 검색 수행 시(자동 저장 ON) 검색어가 기록됨
    • 권한 없음/이미 설정됨/항목 없음 등 사용자 친화적 오류 메시지 확장
  • Chores
    • CI/CD가 develop 브랜치에만 실행되도록 트리거 조정

@doup2001 doup2001 self-assigned this Aug 21, 2025
@doup2001 doup2001 added the ✨ Feature 기능 개발 label Aug 21, 2025
@doup2001 doup2001 linked an issue Aug 21, 2025 that may be closed by this pull request
6 tasks
@github-actions
Copy link

Test Results

34 tests   34 ✅  6s ⏱️
13 suites   0 💤
13 files     0 ❌

Results for commit 3797aab.

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

  • CI: dev-ci-cd.yml에서 push 트리거를 develop 브랜치로 한정.
  • 글로벌: ErrorCode에 검색기록 관련 4개 코드 추가.
  • 도메인: Search 엔티티 신설; User에 isSearch 플래그(기본 true) 및 on/off 메서드 추가.
  • 어댑터(OUT): SearchJpaEntity/Repository 추가, SearchAdapter 추가; UserAdapter.updateUser는 void 반환 및 트랜잭션 처리로 변경, isSearch 업데이트 지원; UserJpaEntity에 isSearch 필드/업데이트 메서드 추가.
  • 애플리케이션: ProductSearchUseCase, ElasticSearchService 제거; SearchUseCase/Port/Service 신설, ES 검색+최근검색 저장/조회/삭제/토글 구현.
  • 웹: SearchController가 SearchUseCase 사용 및 신규 엔드포인트(/my, /auto 등) 추가, Swagger Spec 인터페이스 도입; DevAuthController에서 개발자 생성 시 isSearch(true) 설정.
  • 보안: JWT 필터와 RequestMatcher에 /api/v1/search 경로 적용/권한 매핑 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant JWT as JwtAuthenticationFilter
  participant SC as SearchController
  participant SS as SearchService
  participant UP as UserPort
  participant SP as SearchPort
  participant ES as ElasticsearchOperations

  rect rgb(241,245,249)
  note over C,SC: 제품 검색 (GET /api/v1/search?keyword=... )
  C->>JWT: 요청 (/api/v1/search)
  JWT-->>C: 인증 통과/컨텍스트 설정
  C->>SC: searchProducts(keyword, PageRequest, Principal)
  SC->>SS: searchProducts(keyword, page, size, userId?)
  SS->>ES: ES Query (name/description match, minScore, paging)
  ES-->>SS: Product 문서 목록
  alt userId 존재 AND 사용자 isSearch=true
    SS->>UP: 사용자 조회(userId)
    SS->>SP: saveSearch(Search.of(userId, keyword))
  end
  SS-->>SC: List<Product>
  SC-->>C: ApiResponse<List<ProductDTO>>
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client (USER)
  participant JWT as JwtAuthenticationFilter
  participant SC as SearchController
  participant SS as SearchService
  participant UP as UserPort
  participant SP as SearchPort

  rect rgb(241,245,249)
  note over C,SC: 내 검색기록 조회 (GET /api/v1/search/my)
  C->>JWT: 요청
  JWT-->>C: 인증 통과
  C->>SC: getMySearches(Principal)
  SC->>SS: getMySearches(userId)
  SS->>UP: 사용자 조회
  SS->>SP: getSearches(userId)
  SP-->>SS: List<Search>
  SS-->>SC: List<Search>
  SC-->>C: ApiResponse<List<SearchListResponse>>
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client (USER)
  participant SC as SearchController
  participant SS as SearchService
  participant UP as UserPort
  participant SP as SearchPort

  rect rgb(255,247,237)
  note over C,SS: 검색기록 삭제 (DELETE /api/v1/search/{id})
  C->>SC: deleteSearch(id, Principal)
  SC->>SS: deleteMySearchKeyword(id, userId)
  SS->>UP: 사용자 조회
  SS->>SP: getSearch(id)
  alt 소유자 일치
    SS->>SP: deleteSearch(id, userId)
    SS-->>SC: 완료
    SC-->>C: 200 OK
  else 소유자 불일치 또는 없음
    SS-->>SC: 예외(UNAUTHORIZED_DELETE_SEARCH 또는 SEARCH_NOT_FOUND)
    SC-->>C: 에러 응답
  end
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client (USER)
  participant SC as SearchController
  participant SS as SearchService
  participant UP as UserPort

  rect rgb(240,253,244)
  note over C,UP: 자동 저장 토글 (PUT /api/v1/search/auto/on|off)
  C->>SC: turnOnSearch/turnOffSearch(Principal)
  SC->>SS: turnOnMySearchKeyword/turnOffMySearchKeyword(userId)
  SS->>UP: 사용자 조회
  alt 상태 변경 필요
    SS->>UP: updateUser(user with isSearch toggled)
    SS-->>SC: 완료
    SC-->>C: 200 OK
  else 이미 해당 상태
    SS-->>SC: 예외(ALREADY_ON/ALREADY_OFF)
    SC-->>C: 에러 응답
  end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/product/KIKI-61-ElasticSearch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/site/kikihi/custom/global/response/ErrorCode.java (1)

47-51: 중복 코드값(401_011) 충돌 — 식별자 유일성 보장 필요

REFRESH_TOKEN_NOT_FOUND(401_011)TOKEN_NOT_FOUND_COOKIE(401_011)가 동일 코드값을 사용합니다. 코드값은 로그/지표/클라이언트 분기에서 키로 사용될 수 있어 충돌은 운영 이슈를 유발합니다.

권장 수정안: 순번을 401_009부터 연속으로 재배치하여 중복 제거 및 오름차순 정렬 유지.

-        REFRESH_TOKEN_NOT_FOUND(401_011, HttpStatus.UNAUTHORIZED, "저장된 리프레시 토큰이 존재하지 않습니다."),
-        REFRESH_TOKEN_MISMATCH(401_009, HttpStatus.UNAUTHORIZED, "저장된 리프레시 토큰과 일치하지 않습니다."),
-        EXPIRED_REFRESH_TOKEN(401_010, HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다."),
-        TOKEN_NOT_FOUND_COOKIE(401_011, HttpStatus.UNAUTHORIZED, "쿠키에 리프레시 토큰이 존재하지 않습니다."),
+        REFRESH_TOKEN_MISMATCH(401_009, HttpStatus.UNAUTHORIZED, "저장된 리프레시 토큰과 일치하지 않습니다."),
+        EXPIRED_REFRESH_TOKEN(401_010, HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다."),
+        REFRESH_TOKEN_NOT_FOUND(401_011, HttpStatus.UNAUTHORIZED, "저장된 리프레시 토큰이 존재하지 않습니다."),
+        TOKEN_NOT_FOUND_COOKIE(401_012, HttpStatus.UNAUTHORIZED, "쿠키에 리프레시 토큰이 존재하지 않습니다."),

수정 후, 클라이언트/문서에 노출되는 코드값 변경 영향도(분기 로직, 모니터링 대시보드 매핑)를 함께 점검해 주세요.

src/main/java/site/kikihi/custom/platform/domain/user/User.java (1)

4-8: Builder 사용 시 기본값(true) 유실 가능 — @Builder.Default 추가 필요

@SuperBuilder를 사용하는 경우, 필드 초기값(= true)은 빌더 경로에서 반영되지 않습니다. 현재 of()에서는 명시적으로 .isSearch(true)를 넣었지만, 다른 빌더 생성 경로에서 누락되면 false가 되어 의도치 않게 “최근 검색어 자동 저장”이 꺼질 수 있습니다. @Builder.Default로 안전하게 기본값을 고정해 주세요.

적용 예시:

 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import lombok.Builder;
 import lombok.experimental.SuperBuilder;

@@
-    private boolean isSearch = true;
+    @Builder.Default
+    private boolean isSearch = true;

추가로, 영속 계층(UserJpaEntity)에서도 컬럼 기본값/nullable 제약을 함께 맞춰주면 안전합니다(예: @Column(nullable = false) 및 DB default true).

Also applies to: 27-27

🧹 Nitpick comments (29)
src/main/java/site/kikihi/custom/global/response/ErrorCode.java (1)

31-32: 메시지 띄어쓰기/마침표 일관성 보정 제안

다른 항목들은 문장 끝 마침표가 있는 경우가 많고(예: "…권한이 없습니다."), 여기만 마침표가 없습니다. 또한 “켜져있습니다/꺼져있습니다”는 “켜져 있습니다/꺼져 있습니다”로 띄어쓰는 것이 자연스럽습니다.

-        ALREADY_ON(400_005, HttpStatus.BAD_REQUEST, "이미 검색어 저장 기능이 켜져있습니다"),
-        ALREADY_OFF(400_006, HttpStatus.BAD_REQUEST, "이미 검색어 저장 기능이 꺼져있습니다"),
+        ALREADY_ON(400_005, HttpStatus.BAD_REQUEST, "이미 검색어 저장 기능이 켜져 있습니다."),
+        ALREADY_OFF(400_006, HttpStatus.BAD_REQUEST, "이미 검색어 저장 기능이 꺼져 있습니다."),
src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java (1)

77-77: 개발자 계정의 isSearch(true) 명시 — 도메인 기본값 중복 설정 가능성

User 도메인에서 isSearch 기본값이 true라면(요약에 따르면 default true), 여기의 .isSearch(true)는 중복 설정입니다. 명시적 의도라면 주석으로 이유를 남기고, 아니라면 기본값에 의존해 중복을 제거하는 편이 유지보수에 유리합니다.

         return User.builder()
                 .id(id)
                 .socialId("dev-kakao-id")
                 .email("[email protected]")
                 .profileImage("http://image-url")
                 .phoneNumber("010-1111-1111")
                 .name("kakao개발자")
                 .provider(Provider.KAKAO) // 테스트용 값 (Enum)
                 .role(Role.ADMIN) // 관리자 권한 부여
                 .address(Address.of())
-                .isSearch(true)
                 .build();
src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java (1)

118-121: 검색 엔드포인트에 대해 인증 필터 강제 적용 — 방향성 적절

/api/v1/search에 대해 비회원/회원 구분 로직을 통일하려는 의도가 명확합니다. 다만, 경로 prefix 문자열이 여기와 RequestMatcherHolder 양쪽에 중복 정의되므로 상수화 또는 단일 소스 유지가 필요합니다.

예: 공통 상수로 관리하여 오타/분기 누락을 방지합니다.

 public class JwtAuthenticationFilter extends OncePerRequestFilter {
+    private static final String PRODUCTS_PREFIX = "/api/v1/products";
+    private static final String RECOMMEND_PREFIX = "/api/v1/recommend";
+    private static final String SEARCH_PREFIX = "/api/v1/search";

     @Override
     protected boolean shouldNotFilter(HttpServletRequest request) {
-        if (request.getRequestURI().startsWith("/api/v1/products")) {
+        if (request.getRequestURI().startsWith(PRODUCTS_PREFIX)) {
             return false;
         }
-        if (request.getRequestURI().startsWith("/api/v1/recommend")) {
+        if (request.getRequestURI().startsWith(RECOMMEND_PREFIX)) {
             return false;
         }
-        if (request.getRequestURI().startsWith("/api/v1/search")) {
+        if (request.getRequestURI().startsWith(SEARCH_PREFIX)) {
             return false;
         }
         ...
     }
 }

또한 RequestMatcherHolder에서 관리하는 “항상 필터를 태울 경로” 개념을 도입하면, 경로 추가 시 필터/매처 양쪽을 동기 유지하는 부담이 줄어듭니다.

src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java (1)

15-15: updateUser void 반환 변경 — 영향 범위 확인 및 의도 분리 제안

현재 updateUser(User user) 메서드는 SearchService에서만 호출되며, 실제 구현(UserAdapter)도 isSearch 필드만 갱신하고 있습니다. 범용 업데이트 포트 대신 명확한 목적의 메서드 분리가 안전합니다.

영향도 파악

  • src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java(15행):
    void updateUser(User user);
  • src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java(31–39행):
    @Override
    @Transactional
    public void updateUser(User user) {
        // 기존 로직: isSearch만 갱신
        UserJpaEntity entity = userJpaRepository.findById(user.getId())
            .orElseThrow(() -> new IllegalArgumentException(...));
        entity.updateSearch(user.isSearch());
    }
  • src/main/java/site/kikihi/custom/platform/application/service/SearchService.java(156, 168행):
    userPort.updateUser(user);

개선 제안

  • UserPort 인터페이스에 조회 전용 포트 추가
    public interface UserPort {
        User saveUser(User user);
        // 전체 업데이트는 주의 사용
        void updateUser(User user);
  • // isSearch 토글 전용
    
  • void updateSearch(UUID userId, boolean isSearch);
    …
    
    }
  • UserAdapter에 updateSearch 구현 추가
    @Override
    @Transactional
    public void updateSearch(UUID userId, boolean isSearch) {
        UserJpaEntity entity = userJpaRepository.findById(userId)
            .orElseThrow(() -> new IllegalArgumentException(...));
        entity.updateSearch(isSearch);
    }
  • SearchService에서 호출부 교체
    - userPort.updateUser(user);
    + userPort.updateSearch(user.getId(), user.isSearch());

(필요 시) 범용 updateUser는 Deprecated 처리하거나 제거하여 오용을 방지하세요. 전체 호출부 일괄 교체 패치가 필요하면 도와드리겠습니다.

src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java (1)

83-86: 메서드 네이밍 경미 개선 제안: updateSearch → setSearchEnabled

의미가 조금 더 직접적으로 전달됩니다. 기능은 간단/명확하며 JPA 변경감지에도 적합합니다.

src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java (1)

66-68: /api/v1/search GET 매핑이 중복으로 등록되어 있습니다

이미 Line 42에 동일한 엔트리가 있어 중복입니다. 기능적으로 큰 문제는 없지만 유지보수성을 위해 제거를 권장합니다.

-            // 검색 (임시로 개방)
-            new RequestInfo(GET, "/api/v1/search", null)
src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java (2)

31-33: 널 가드 추가 제안(user, user.id)

Repository의 findById(null)은 즉시 예외가 납니다. 초기에 명시적으로 검증하면 실패 원인이 더 분명해집니다.

     @Transactional
     public void updateUser(User user) {
+        java.util.Objects.requireNonNull(user, "user must not be null");
+        java.util.Objects.requireNonNull(user.getId(), "user.id must not be null");

추가 import:

import java.util.Objects;

34-36: 예외 타입을 글로벌 예외 정책과 정렬하세요

현재 IllegalArgumentException(ErrorCode.USER_NOT_FOUND.getMessage())는 글로벌 핸들러에서 코드 매핑이 어려울 수 있습니다. 프로젝트 공용의 예외 타입(예: Platform/Business/Api 예외 + ErrorCode)을 사용해 일관된 에러 응답을 보장하는 것을 권장합니다.

해당 프로젝트에서 ErrorCode를 감싸는 표준 예외 클래스가 무엇인지 알려주시면 구체적인 변경안을 드리겠습니다.

src/main/java/site/kikihi/custom/platform/domain/search/Search.java (1)

24-30: 키워드 유효성 검증(공백/길이) 추가 권장

도메인 팩토리에서 최소한의 유효성 검사를 수행하면 서비스/웹 레이어 중복 검증을 줄일 수 있습니다. 예: 공백만 입력, 과도한 길이.

     public static Search of(UUID userId, String keyword) {
-        return Search.builder()
-                .userId(userId)
-                .keyword(keyword)
-                .build();
+        String normalized = keyword == null ? "" : keyword.strip();
+        if (normalized.isEmpty()) {
+            throw new IllegalArgumentException("keyword must not be blank");
+        }
+        if (normalized.length() > 100) {
+            throw new IllegalArgumentException("keyword length must be <= 100");
+        }
+        return Search.builder()
+                .userId(userId)
+                .keyword(normalized)
+                .build();
     }

최대 길이(여기서는 100)는 서비스 요구사항에 맞춰 상수로 분리해도 좋습니다.

src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java (1)

21-26: 리스트 변환 유틸리티 from(List<...>)도 적절합니다

Stream + toList로 간결하게 처리되어 있습니다. 필요 시 null-safe 처리(Optional/emptyList) 여부는 컨트롤러/서비스 계약에 맞춰 검토하면 됩니다.

src/main/java/site/kikihi/custom/platform/domain/user/User.java (2)

41-42: 중복 방지 관점에서 of()의 .isSearch(true)는 선택 사항

위와 같이 @Builder.Default를 적용하면 of()에서의 .isSearch(true)는 기능적으로 중복입니다. 가독성상 초기 의도를 드러내기 위해 유지해도 무방하나, 일관성 유지를 위해 제거하는 것도 고려해 보세요.

-                .isSearch(true)

45-52: 토글 메서드 간소화 제안

turnOnSearch()/turnOffSearch() 두 메서드는 동작이 단순 토글입니다. 단일 메서드로 압축하면 사용성이 좋아집니다. (호출부 영향이 있으므로 선택 사항)

예시:

-    public void turnOnSearch() {
-        isSearch = true;
-    }
-
-    public void turnOffSearch() {
-        isSearch = false;
-    }
+    public void setSearchSavingEnabled(boolean enabled) {
+        this.isSearch = enabled;
+    }

또는 두 메서드를 유지하되 내부에서 공용 메서드를 호출하도록 위임해도 됩니다.

src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java (1)

16-17: 최근 검색어 조회에 페이징/상한 필요

List<Search> getSearches(UUID userId)는 전체 로드를 유발할 수 있어 유저별 검색 기록이 늘면 성능/메모리 문제가 생깁니다. 포트 레벨에서 페이지/사이즈(혹은 limit) 파라미터를 받도록 확장하는 것을 권장합니다. (Spring 의존 회피를 위해 Pageable 대신 원시 파라미터 권장)

예시:

-    List<Search> getSearches(UUID userId);
+    List<Search> getSearches(UUID userId, int page, int size);
src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java (1)

10-15: 페이징 쿼리 및 삭제 결과 반환 개선 제안

  • 대용량 데이터 고려 시 페이징 메서드 제공이 유용합니다.
  • deleteByIdAndUserId는 성공/미실행 구분을 위해 삭제 건수(long) 반환을 권장합니다.

예시:

+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
@@
-    List<SearchJpaEntity> findByUserIdOrderByCreatedAtDesc(UUID userId);
+    List<SearchJpaEntity> findByUserIdOrderByCreatedAtDesc(UUID userId);
+    Page<SearchJpaEntity> findByUserIdOrderByCreatedAtDesc(UUID userId, Pageable pageable);
@@
-    void deleteByIdAndUserId(Long id, UUID userId);
+    long deleteByIdAndUserId(Long id, UUID userId);

서비스 레이어에서는 long 반환값(0/1)에 따라 권한 오류/리소스 없음 등을 분기할 수 있습니다.

src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java (3)

3-7: 컬럼 제약 추가를 위한 import 보강

@Column을 활용해 not-null/length 제약을 명시하는 것을 권장합니다.

 import jakarta.persistence.Entity;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
+import jakarta.persistence.Column;

27-29: DB 제약 명시: NOT NULL / 길이 제한

userId, keyword는 비즈니스적으로 null이 허용되지 않아 보입니다. 길이 제한도 함께 두면 인덱스 및 저장 효율에 유리합니다.

-    private UUID userId;
+    @Column(nullable = false)
+    private UUID userId;
 
-    private String keyword;
+    @Column(nullable = false, length = 200)
+    private String keyword;

추가로, “최근 검색어 조회”는 userId, createdAt DESC 액세스가 많으므로 DB 인덱스(복합 혹은 단일) 추가를 권장합니다. BaseTimeEntity의 실제 컬럼명(createdAt vs created_at)에 맞춰 @Table(indexes = …) 또는 마이그레이션 스크립트로 적용해 주세요.


31-37: 엔티티 변환 시 입력 정규화 고려

keyword에 대해 좌우 공백 제거/빈 문자열 차단/대소문자 규칙(옵션)을 도메인 또는 서비스 계층에서 보장하면 중복/노이즈 데이터가 줄어듭니다. 현재 변환은 그대로 전달하므로 저장 전 검증 로직이 없다면 추가를 권장합니다.

src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java (4)

18-20: 검색 결과의 페이지 메타데이터 노출 여부 검토

searchProducts(String keyword, int page, int size, UUID userId)List<Product>만 반환하면 총건수/다음 페이지 여부 등의 메타데이터가 사라져 클라이언트가 추가 호출 전략을 세우기 어렵습니다.

옵션:

  • 도메인 친화적 DTO(예: SearchResult<T> { List<T> items; int page; int size; boolean hasNext; long total; })로 반환
  • 또는 Slice 패턴 도입(의존성 노출이 부담되면 포트 계층은 원시 타입 유지 + 서비스/컨트롤러에서 어댑트)

22-23: 나의 검색어 조회에도 페이징/상한 도입 권장

getMySearches(UUID userId) 역시 무제한 조회입니다. 포트/유즈케이스 어느 층에서든 page/size 또는 limit 기반의 상한을 도입하면 안정적입니다.

-    List<Search> getMySearches(UUID userId);
+    List<Search> getMySearches(UUID userId, int page, int size);

30-32: 불린 의미 명시화 제안

checkSearch(UUID userId)는 무엇을 “체크”하는지 모호할 수 있습니다. isSearchAutoSaveEnabled(UUID userId)처럼 의미를 드러내면 호출부 가독성이 좋아집니다. (호환성 영향이 있으므로 선택 사항)

-    boolean checkSearch(UUID userId);
+    boolean isSearchAutoSaveEnabled(UUID userId);

33-37: 메서드 명명 개선 제안: ‘검색어 자동 저장’ 토글로 변경

이 메서드들은 “특정 키워드”가 아니라 “최근 검색어 자동 저장 기능”의 ON/OFF 토글이므로, 명확한 의미 전달을 위해 네이밍을 아래와 같이 변경할 것을 권장합니다. 이 때 인터페이스·구현체·컨트롤러 매핑·Swagger 스펙까지 일관되게 갱신해야 합니다.

추가로, 호출부가 모두 업데이트되었는지 아래 위치를 점검해 주세요.

  • 인터페이스

    • src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java
      • 34행: void turnOffMySearchKeyword(UUID userId);
      • 37행: void turnOnMySearchKeyword(UUID userId);
  • 구현체

    • src/main/java/site/kikihi/custom/platform/application/service/SearchService.java
      • 148행: public void turnOffMySearchKeyword(UUID userId) { … }
      • 161행: public void turnOnMySearchKeyword(UUID userId) { … }
  • 컨트롤러

    • src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java
      • 110행: service.turnOnMySearchKeyword(principalDetails.getId());
      • 124행: service.turnOffMySearchKeyword(principalDetails.getId());
  • Swagger/OpenAPI 스펙 (YAML 또는 애노테이션)
    turnOnMySearchKeyword / turnOffMySearchKeyword 엔드포인트 명, 설명 등

예시 diff:

-    void turnOffMySearchKeyword(UUID userId);
+    void turnOffSearchAutoSave(UUID userId);
@@
-    void turnOnMySearchKeyword(UUID userId);
+    void turnOnSearchAutoSave(UUID userId);

– 호출부, 컨트롤러 매핑, Swagger 스펙에도 동일하게 반영 바랍니다.
– API 변경 범위(호환성)에 유의하시고, 클라이언트/프론트팀과 사전 조율을 권장합니다.

src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java (1)

48-50: 오탈자: 메서드명 deleteALlSearchdeleteAllSearch로 정정 권장

ALl(대문자 L 소문자 l) 오탈자가 퍼블릭 API(Port/Adapter/Service) 전반에 퍼져 유지보수/검색성에 악영향을 줍니다. 일괄 리네이밍을 권장드립니다.

-    public void deleteALlSearch(UUID userId) {
+    public void deleteAllSearch(UUID userId) {
         repository.deleteAllByUserId(userId);
     }

SearchService 호출부도 함께 변경이 필요합니다(해당 파일에 제안 포함).

src/main/java/site/kikihi/custom/platform/application/service/SearchService.java (4)

5-5: 미사용 의존성 제거 (ProductESRepository)

필드가 사용되지 않습니다. 불필요한 의존성은 제거해 주세요(주입/테스트/가독성 면에서 이득).

-import site.kikihi.custom.platform.adapter.out.elasticSearch.ProductESRepository;
@@
-    private final ProductESRepository productESRepository;

Also applies to: 33-34


39-41: 상수화 및 네이밍: minScore를 클래스 상수로 승격

변하지 않는 값은 static final 상수로 두는 편이 명확합니다(네이밍도 관례 반영).

-    private final Float minScore = 0.001f;
+    private static final float MIN_SCORE = 0.001f;
@@
-                .withMinScore(minScore)  // <<-- 추가됨
+                .withMinScore(MIN_SCORE)

Also applies to: 56-61


122-130: 오탈자 연쇄 수정: deleteALlSearchdeleteAllSearch

앞서 어댑터/포트와 함께 메서드명을 정정해야 합니다.

-        port.deleteALlSearch(user.getId());
+        port.deleteAllSearch(user.getId());

147-158: 쓰기 메서드에 트랜잭션 추가 및 상태 불변 시 명시적 피드백 고려

  • on/off, 삭제 등 쓰기 연산은 트랜잭션 경계가 있는 편이 안전합니다.
  • 이미 ON/OFF 상태일 때는 조용히 통과보다는 명시적 피드백(ErrorCode.ALREADY_ON/ALREADY_OFF)도 고려해보세요(요구사항에 따라 선택).
@@
-    public void turnOffMySearchKeyword(UUID userId) {
+    @org.springframework.transaction.annotation.Transactional
+    public void turnOffMySearchKeyword(UUID userId) {
@@
-        if (user.isSearch()) {
+        if (user.isSearch()) {
             user.turnOffSearch();
             userPort.updateUser(user);
+        } else {
+            // 요구사항에 따라 주석 해제
+            // throw new IllegalStateException(ErrorCode.ALREADY_OFF.getMessage());
         }
@@
-    public void turnOnMySearchKeyword(UUID userId) {
+    @org.springframework.transaction.annotation.Transactional
+    public void turnOnMySearchKeyword(UUID userId) {
@@
-        if (!user.isSearch()) {
+        if (!user.isSearch()) {
             user.turnOnSearch();
             userPort.updateUser(user);
+        } else {
+            // 요구사항에 따라 주석 해제
+            // throw new IllegalStateException(ErrorCode.ALREADY_ON.getMessage());
         }

Also applies to: 160-171

src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java (2)

30-32: Swagger 문서화: PageRequest 파라미터 인식 개선(@ParameterObject)

커스텀 PageRequest는 기본적으로 스키마에 풀리지 않습니다. @ParameterObject를 붙여주면 쿼리 파라미터(page/size)가 문서에 노출됩니다.

-            PageRequest pageRequest,
+            @org.springdoc.core.annotations.ParameterObject PageRequest pageRequest,

34-44: 키워드 정규화(트림) 후 서비스 호출

불필요한 공백 저장/검색을 줄이기 위해 컨트롤러에서 1차 정규화를 권장합니다. 서비스에서도 빈 키워드는 빈 결과를 반환하도록 제안해 두었습니다.

-        /// 유저가 없다면 null 저장
-        UUID userId = principalDetails != null ? principalDetails.getId() : null;
+        // 키워드 정규화
+        String sanitizedKeyword = keyword == null ? "" : keyword.trim();
+        /// 유저가 없다면 null 저장
+        UUID userId = principalDetails != null ? principalDetails.getId() : null;
@@
-        List<Product> productList = service.searchProducts(keyword, pageRequest.getPage(), pageRequest.getSize(), userId);
+        List<Product> productList = service.searchProducts(sanitizedKeyword, pageRequest.getPage(), pageRequest.getSize(), userId);
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java (1)

24-29: Swagger 문서화: PageRequest 파라미터에 @ParameterObject 적용

스펙 인터페이스에도 @ParameterObject를 붙이면 문서 일관성이 좋아집니다.

     ApiResponse<List<ProductListResponse>> searchProducts(
             @Parameter(example = "하우징")
             @RequestParam("keyword") String keyword,
-            PageRequest pageRequest,
+            @org.springdoc.core.annotations.ParameterObject PageRequest pageRequest,
             @AuthenticationPrincipal PrincipalDetails principalDetails
     );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7db77e1 and 3797aab.

📒 Files selected for processing (21)
  • .github/workflows/dev-ci-cd.yml (1 hunks)
  • src/main/java/site/kikihi/custom/global/response/ErrorCode.java (4 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/DevAuthController.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java (2 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaRepository.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java (3 hunks)
  • src/main/java/site/kikihi/custom/platform/application/in/product/ProductSearchUseCase.java (0 hunks)
  • src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/application/service/ElasticSearchService.java (0 hunks)
  • src/main/java/site/kikihi/custom/platform/application/service/SearchService.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/domain/search/Search.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/domain/user/User.java (2 hunks)
  • src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java (1 hunks)
  • src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java (2 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/site/kikihi/custom/platform/application/in/product/ProductSearchUseCase.java
  • src/main/java/site/kikihi/custom/platform/application/service/ElasticSearchService.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java (1)
src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (1)
  • Getter (22-99)
src/main/java/site/kikihi/custom/platform/adapter/out/jpa/search/SearchJpaEntity.java (1)
src/main/java/site/kikihi/custom/platform/domain/search/Search.java (1)
  • AllArgsConstructor (11-32)
src/main/java/site/kikihi/custom/platform/application/service/SearchService.java (1)
src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (1)
  • Getter (22-99)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (14)
src/main/java/site/kikihi/custom/global/response/ErrorCode.java (3)

74-76: 검색 도메인 NotFound 코드 추가 적절

도메인별 404 세분화는 API 클라이언트 처리에 유용합니다. 메시지 서술과 톤도 기존 항목들과 잘 맞습니다.


88-89: 포맷팅 변경으로 보이며 기능 영향 없음

세미콜론/포맷팅 외 변화는 없어 보입니다.


61-61: 전역 예외 처리 매핑 확인 완료

  • SearchService.java(113행): ErrorCode.UNAUTHORIZED_DELETE_SEARCHIllegalArgumentException으로 throw
  • SearchService.java(185행): ErrorCode.SEARCH_NOT_FOUNDNoSuchElementException으로 throw
  • GlobalExceptionHandler.java(37행): IllegalArgumentException@ExceptionHandler({IllegalStateException.class, IllegalArgumentException.class})로 처리
  • GlobalExceptionHandler.java(51행): NoSuchElementException@ExceptionHandler({NoSuchElementException.class, NoResourceFoundException.class})로 처리

따라서, 신규 권한 에러코드가 서비스 레이어에서 올바르게 throw되고 전역 예외 핸들러에서 적절히 매핑되어 있습니다.

src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java (2)

64-65: from(User) 매핑에 isSearch 추가 LGTM

도메인 기본값과 엔티티 플래그 동기화가 명확해졌습니다.


79-80: toDomain 매핑에 isSearch 추가 LGTM

양방향 매핑이 맞춰져 있어 일관성이 확보되었습니다.

src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java (1)

41-46: 컨트롤러에서 /api/v1/search 관련 매핑이 전혀 검색되지 않습니다. 아래 사항을 직접 확인해 주세요:

  • 애노테이션 기반 매핑이 아닌 WebFlux RouterFunction 같은 기능형 라우팅을 사용 중인지
  • 컨트롤러가 다른 패키지나 모듈에 위치해 있진 않은지
  • YAML/프로퍼티 설정 등으로 경로를 외부에서 주입하고 있진 않은지

수동으로 코드베이스를 점검해 주시고, 매핑이 의도적 생략인지 혹은 RequestMatcherHolder 설정과 불일치하는지 검토 부탁드립니다.

src/main/java/site/kikihi/custom/platform/adapter/out/UserAdapter.java (2)

37-39: 변경감지로 isSearch만 갱신하는 방식 LGTM

필요한 필드만 조용히 업데이트하는 패턴이라 성능/간결성 측면에서 적절합니다.


30-39: updateUser 반환형 변경 영향 없음 확인

updateUser의 반환형을 User에서 void로 변경했으나, 호출부에서 반환값을 사용 중인 부분이 전혀 발견되지 않아 문제없습니다.

  • 호출부 예시
    • src/main/java/site/kikihi/custom/platform/application/service/SearchService.java:156
    • src/main/java/site/kikihi/custom/platform/application/service/SearchService.java:168
src/main/java/site/kikihi/custom/platform/domain/search/Search.java (1)

11-22: 도메인 구조 전반은 간결하고 목적에 부합합니다

BaseDomain 상속 + 불변 빌더, 필요한 최소 필드로 잘 정리되어 있습니다.

src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/search/SearchListResponse.java (1)

13-19: DTO 변환 팩토리(from) 구현 LGTM

도메인 → 응답 DTO 매핑이 명확하고, 레코드 + 빌더 조합도 문제 없습니다.

src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java (2)

21-22: 메서드명 오타 수정 완료 및 반영 확인

아래 파일들에서 deleteALlSearchdeleteAllSearches로 일관되게 변경된 것을 확인했습니다.

  • src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java: 인터페이스 시그니처 수정
  • src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java: 구현체 오버라이드 메서드명 수정
  • src/main/java/site/kikihi/custom/platform/application/service/SearchService.java: 호출부 변경

모든 구현부와 사용처가 올바르게 업데이트되었으므로 추가 작업은 필요 없습니다.


13-14: 추가 확인: SearchPort.getSearch 실제 호출 위치 검증

현재 SearchPort.getSearch(Long) 선언(src/main/java/site/kikihi/custom/platform/application/out/search/SearchPort.java) 및 구현(src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java), 그리고 서비스 내 프라이빗 래퍼 메서드(src/main/java/site/kikihi/custom/platform/application/service/SearchService.java)가 확인되었습니다.
이 메서드가 실제로 어디에서 호출되는지 추가 검증이 필요합니다.

아래 스크립트를 실행하여 코드베이스 전반에서 getSearch(...) 호출을 찾아주세요:

#!/bin/bash
set -euo pipefail
rg -nP '\.\s*getSearch\s*\(' -n src/main/java

– 호출 위치가 없다면 해당 포트 메서드는 제거를 고려할 수 있습니다.
– 호출 위치가 존재한다면 용도에 맞춰 계속 유지하거나, 불필요 호출 여부를 별도 검토해 주세요.

src/main/java/site/kikihi/custom/platform/adapter/out/SearchAdapter.java (1)

35-39: Stream#toList() 사용 적합 확인

build.gradle의 Java 툴체인 설정에서 languageVersion = JavaLanguageVersion.of(17)로 지정되어 있어, JDK 17 환경에서 Stream#toList() 사용이 가능합니다. 해당 코드는 변경하지 않으셔도 됩니다.

src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java (1)

48-58: 검증 완료: /api/v1/search 하위 경로는 모두 인증 필수

  • RequestMatcherHolder 의 REQUEST_INFO_LIST 에는
    new RequestInfo(GET, "/api/v1/search", null)
    minRole=null 로 설정되어 있어, 오직 /api/v1/search 단일 엔드포인트만 익명 접근이 허용됩니다.
  • /api/v1/search/my, /api/v1/search/delete, /api/v1/search/auto-save 등 하위 경로는 패턴에 포함되지 않아
    anyRequest().authenticated() 규칙에 따라 반드시 인증을 거쳐야 합니다.
  • 따라서 Controller 의 @AuthenticationPrincipal PrincipalDetails principalDetails 는 null 이 될 수 없으므로 NPE 우려가 없습니다.

Comment on lines 10 to 13
on:
push:
branches: [ "develop","feat/product/KIKI-61-ElasticSearch"]
branches: [ "develop"]

Copy link

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)에서만 실행되도록 가드해 주세요.

아래처럼 트리거와 배포 단계 조건을 분리하는 것을 제안드립니다.

 on:
-  push:
-    branches: [ "develop"]
+  push:
+    branches: [ "develop" ]
+  pull_request:
+    branches: [ "develop" ]
+  workflow_dispatch:

그리고 Docker 로그인/푸시 단계에 조건을 추가해 PR 이벤트에서는 스킵되도록 합니다.

-      - name: Docker 로그인
+      - name: Docker 로그인
+        if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
         uses: docker/login-action@v3
         with:
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_ACCESS_TOKEN}}

-      - name: Docker 이미지 Push
+      - name: Docker 이미지 Push
+        if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
         uses: docker/build-push-action@v4
         with:
           context: .
           dockerfile: Dockerfile
           push: true
           tags: ${{secrets.DOCKER_USERNAME}}/server:latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
push:
branches: [ "develop","feat/product/KIKI-61-ElasticSearch"]
branches: [ "develop"]
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
workflow_dispatch:
Suggested change
on:
push:
branches: [ "develop","feat/product/KIKI-61-ElasticSearch"]
branches: [ "develop"]
- name: Docker 로그인
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_ACCESS_TOKEN}}
- name: Docker 이미지 Push
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
uses: docker/build-push-action@v4
with:
context: .
dockerfile: Dockerfile
push: true
tags: ${{secrets.DOCKER_USERNAME}}/server:latest
🤖 Prompt for AI Agents
.github/workflows/dev-ci-cd.yml around lines 10 to 13: currently only a push
trigger for develop is declared which means PRs/feature branches won't run CI;
add a pull_request trigger (e.g., for branches: [develop]) so PR events run the
workflow, and then guard deployment/push steps by conditioning them to run only
on push to refs/heads/develop (e.g., job or step if: github.event_name == 'push'
&& github.ref == 'refs/heads/develop') and add an additional condition to skip
Docker login/push when github.event_name == 'pull_request' so those steps only
execute on actual develop pushes.

Comment on lines +71 to +76
summary = "검색어 자동저장 기능 ON API",
description = "JWT를 바탕으로 자동저장을 킵니다."
)
ApiResponse<Void> turnOnSearch(
@AuthenticationPrincipal PrincipalDetails principalDetails
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

오타 수정: “킵니다” → “켭니다”

사용자 노출 문구이므로 맞춤법을 맞춰 주세요.

     @Operation(
             summary = "검색어 자동저장 기능 ON API",
-            description = "JWT를 바탕으로 자동저장을 킵니다."
+            description = "JWT를 바탕으로 자동저장을 켭니다."
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
summary = "검색어 자동저장 기능 ON API",
description = "JWT를 바탕으로 자동저장을 킵니다."
)
ApiResponse<Void> turnOnSearch(
@AuthenticationPrincipal PrincipalDetails principalDetails
);
@Operation(
summary = "검색어 자동저장 기능 ON API",
description = "JWT를 바탕으로 자동저장을 켭니다."
)
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java
around lines 71 to 76, the Korean description contains a typo: change "자동저장을
킵니다." to the correct "자동저장을 켭니다." Update the string literal in the description
annotation accordingly, preserving surrounding spacing and formatting.

Comment on lines +44 to +45
private boolean isSearch;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

isSearch 컬럼의 기본값/DDL을 명시해 의도(기본 true)를 보장하세요

도메인(User)의 기본값이 true라면 JPA 엔티티/DB에서도 불일치가 없도록 해야 합니다. 현재 boolean 기본값은 false이며 @column 제약이 없어 기존 레코드 마이그레이션 시 의도치 않게 false가 저장될 수 있습니다. 신규/기존 유저 모두에서 “자동저장 기본 활성화”를 보장하기 위해 다음을 권장합니다:

  • 애플리케이션 레벨: 필드 초기값을 true로 두고, not null 제약 추가.
  • DB 레벨: 컬럼 추가 마이그레이션 시 DEFAULT TRUE + NOT NULL, 그리고 기존 NULL/미설정 레코드는 TRUE로 백필.

권장 변경안:

-    private boolean isSearch;
+    @Column(nullable = false)
+    private boolean isSearch = true;

마이그레이션 예시(MySQL 기준, 환경에 맞게 조정 필요):

  • ALTER TABLE users ADD COLUMN is_search TINYINT(1) NOT NULL DEFAULT 1;
  • UPDATE users SET is_search = 1 WHERE is_search IS NULL;

엔티티/스키마 동기화가 완료되었는지 확인 부탁드립니다. 필요한 경우 마이그레이션 스크립트 작성 도와드릴게요.


🏁 Script executed:

#!/bin/bash
# Check JPA entity field declaration for isSearch
rg -nP '\bprivate\s+boolean\s+isSearch\b' -C2 src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java
# Check for existing @Column annotation
rg -nP '@Column' -n src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java
# Inspect domain model for isSearch default value or initialization
rg -nP 'isSearch' -C2 src/main/java/site/kikihi/custom/platform/domain/**/*.java

Length of output: 2949


🏁 Script executed:

#!/bin/bash
# Show @PrePersist method and surrounding code for isSearch default handling
rg -n '@PrePersist' -C5 src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java

Length of output: 359


JPA 엔티티 isSearch 필드에 기본값(true)과 NOT NULL 제약 추가 필요

도메인 모델(User)에서는 isSearch 기본값이 true로 설정되어 있으나, JPA 엔티티에는 기본값 할당 및 NOT NULL 제약이 없어 신규/기존 레코드 마이그레이션 시 false 또는 NULL로 저장될 수 있습니다.

수정 대상

  • src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java: isSearch 필드 정의

추천 변경안

-    private boolean isSearch;
+    @Column(name = "is_search", nullable = false)
+    private boolean isSearch = true;

추가 마이그레이션 예시 (MySQL 기준):

  • ALTER TABLE users ADD COLUMN is_search TINYINT(1) NOT NULL DEFAULT 1;
  • UPDATE users SET is_search = 1 WHERE is_search IS NULL;

엔티티와 DB 스키마가 완전히 동기화되었는지 확인해 주세요. 필요한 경우 migration 스크립트 작성 지원해 드리겠습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private boolean isSearch;
@Column(name = "is_search", nullable = false)
private boolean isSearch = true;
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/out/jpa/user/UserJpaEntity.java
around lines 44-45, the isSearch field lacks a default value and NOT NULL
constraint in the JPA entity causing new/existing rows to be stored as
false/NULL; update the entity field to initialize it to true and annotate with
@Column(name = "is_search", nullable = false) so Hibernate maps a non-null
column and default behavior, and then create/run a DB migration (e.g., ALTER
TABLE users ADD COLUMN is_search TINYINT(1) NOT NULL DEFAULT 1; and UPDATE users
SET is_search = 1 WHERE is_search IS NULL;) to backfill existing rows and ensure
schema/entity synchronization.

Comment on lines +41 to +44
@Override
public void deleteSearch(Long searchId, UUID userId) {
repository.deleteByIdAndUserId(searchId, userId);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

권한 검증이 실질적으로 동작하지 않을 수 있습니다 — 삭제 건수 확인 로직 추가 제안

deleteByIdAndUserId는 보통 예외를 던지지 않고 0건 삭제로 끝납니다. 현재 어댑터는 반환값을 확인하지 않아, 서비스 단에서 항상 성공처럼 보일 수 있습니다(무단 삭제/타 사용자 리소스에 대한 요청을 구분 못 함).

아래처럼 삭제 건수를 확인하고 0건이면 EmptyResultDataAccessException을 던지도록 바꾸면, 서비스가 이를 UNAUTHORIZED_DELETE_SEARCH로 매핑할 수 있습니다.

@@
+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);
+        }
     }

참고: 위 변경을 위해 SearchJpaRepository#deleteByIdAndUserId의 시그니처를 voidlong으로 조정이 필요합니다. 원하시면 레포지토리/포트/서비스까지 일괄 변경 PR 패치도 드릴게요.

Comment on lines +42 to +76
/// 키워드 검색 (name, description)
@Override
public List<Product> 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();

// bool 쿼리
Query boolQuery = BoolQuery.of(b -> b
.should(nameMatch)
.should(descMatch)
.minimumShouldMatch("1")
)._toQuery();

// NativeQuery
NativeQuery query = NativeQuery.builder()
.withQuery(boolQuery)
.withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size)
.withMinScore(minScore) // <<-- 추가됨
.build();

/// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기
if (userId != null) {

/// 유저 조회
User user = getUser(userId);

/// 자동저장이 ON인 유저만 저장한다.
if (user.isSearch()) {

/// DB에 최신 검색어 저장하기
Search search = Search.of(user.getId(), keyword);
port.saveSearch(search);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

빈/공백 키워드 처리 및 검색기록 저장 실패의 비차단화

  • 공백 키워드는 ES 쿼리 오류/노이즈를 만들 수 있으므로 사전에 걸러 빈 리스트를 반환하는 편이 안전합니다.
  • 최근검색 저장 실패가 본 검색 API 실패로 번지지 않도록 try/catch로 감싸 비차단화하는 것을 권장합니다.
     @Override
     public List<Product> 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();
+        // 키워드 정규화 및 빈/공백 방지
+        String sanitized = keyword == null ? "" : keyword.trim();
+        if (sanitized.isEmpty()) {
+            return List.of();
+        }
+
+        // match 쿼리 구성
+        Query nameMatch = MatchQuery.of(m -> m.field("name").query(sanitized))._toQuery();
+        Query descMatch = MatchQuery.of(m -> m.field("description").query(sanitized))._toQuery();
@@
-        /// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기
+        /// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기(비차단)
         if (userId != null) {
@@
-            /// 자동저장이 ON인 유저만 저장한다.
+            /// 자동저장이 ON인 유저만 저장한다.
             if (user.isSearch()) {
-
-                /// DB에 최신 검색어 저장하기
-                Search search = Search.of(user.getId(), keyword);
-                port.saveSearch(search);
+                /// DB에 최신 검색어 저장하기 (실패해도 검색은 진행)
+                try {
+                    port.saveSearch(Search.of(user.getId(), sanitized));
+                } catch (Exception ignore) {
+                    // logging 필요 시 여기에 추가
+                }
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// 키워드 검색 (name, description)
@Override
public List<Product> 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();
// bool 쿼리
Query boolQuery = BoolQuery.of(b -> b
.should(nameMatch)
.should(descMatch)
.minimumShouldMatch("1")
)._toQuery();
// NativeQuery
NativeQuery query = NativeQuery.builder()
.withQuery(boolQuery)
.withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size)
.withMinScore(minScore) // <<-- 추가됨
.build();
/// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기
if (userId != null) {
/// 유저 조회
User user = getUser(userId);
/// 자동저장이 ON인 유저만 저장한다.
if (user.isSearch()) {
/// DB에 최신 검색어 저장하기
Search search = Search.of(user.getId(), keyword);
port.saveSearch(search);
}
}
/// 키워드 검색 (name, description)
@Override
public List<Product> searchProducts(String keyword, int page, int size, UUID userId) {
// 키워드 정규화 및 빈/공백 방지
String sanitized = keyword == null ? "" : keyword.trim();
if (sanitized.isEmpty()) {
return List.of();
}
// match 쿼리 구성
Query nameMatch = MatchQuery.of(m -> m.field("name").query(sanitized))._toQuery();
Query descMatch = MatchQuery.of(m -> m.field("description").query(sanitized))._toQuery();
// bool 쿼리
Query boolQuery = BoolQuery.of(b -> b
.should(nameMatch)
.should(descMatch)
.minimumShouldMatch("1")
)._toQuery();
// NativeQuery
NativeQuery query = NativeQuery.builder()
.withQuery(boolQuery)
.withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size)
.withMinScore(minScore) // <<-- 추가됨
.build();
/// 로그인 한 유저가 확인한다면, 최근 검색 기록 DB에 저장하기(비차단)
if (userId != null) {
/// 유저 조회
User user = getUser(userId);
/// 자동저장이 ON인 유저만 저장한다.
if (user.isSearch()) {
/// DB에 최신 검색어 저장하기 (실패해도 검색은 진행)
try {
port.saveSearch(Search.of(user.getId(), sanitized));
} catch (Exception ignore) {
// logging 필요 시 여기에 추가
}
}
}
// ...execute the built query and return results...
}
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/application/service/SearchService.java
around lines 42 to 76, reject blank/whitespace keywords early (trim keyword and
if empty return Collections.emptyList()) to avoid creating invalid ES queries,
and wrap the recent-search persistence block in a try/catch so any exceptions
from port.saveSearch(...) are caught and logged (do not rethrow) so
search-history failures do not break the main search response.

Comment on lines +59 to +61
.withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size)
.withMinScore(minScore) // <<-- 추가됨
.build();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

페이지 인덱스 기준(0/1) 확인 필요

PageRequest.of(page, size)는 0-base 인덱스를 기대합니다. 컨트롤러가 사용하는 커스텀 PageRequest(global)도 0-base인지 꼭 확인해 주세요. 1-base라면 첫 페이지 누락/중복이 납니다.

검증 스크립트:


🏁 Script executed:

#!/bin/bash
# 글로벌 PageRequest 구현/기본값 확인
ast-grep --pattern $'class PageRequest {\n  $$$\n}' 2>/dev/null || true
rg -nP --glob '!**/build/**' -C2 'class\s+PageRequest\b|\bgetPage\s*\(|\bpage\s*=\s*' site/kikihi/custom/global

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Locate custom PageRequest class definitions in source code
rg -nP --max-columns=200 -C2 'class\s+PageRequest\b' src/main/java
# Locate usages of PageRequest.of
rg -nP --max-columns=200 -C2 'PageRequest\.of\(' src/main/java
# Show imports and context in SearchService.java
rg -g 'SearchService.java' -l src/main/java | xargs -I {} sh -c "echo 'File: {}'; sed -n '1,50p' {}"

Length of output: 3303


🏁 Script executed:

#!/bin/bash
# Print imports and initial context of SearchService.java
sed -n '1,50p' src/main/java/site/kikihi/custom/platform/application/service/SearchService.java

# Print full PageRequest class definition
sed -n '1,200p' src/main/java/site/kikihi/custom/global/response/page/PageRequest.java

# Locate static of method in custom PageRequest
rg -nP 'static\s+PageRequest\s+of' -C2 src/main/java/site/kikihi/custom/global/response/page/PageRequest.java

Length of output: 3101


ProductController에서 custom PageRequest를 service로 전달하는 부분을 확인하기 위한 스크립트입니다.
아래 명령을 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# ProductController 상단 및 엔드포인트 정의 확인
sed -n '1,200p' src/main/java/site/kikihi/custom/platform/adapter/in/web/ProductController.java

# searchProducts 메서드 호출부 확인
rg -nP 'searchProducts' -C3 src/main/java/site/kikihi/custom/platform/adapter/in/web/ProductController.java

Length of output: 4848


SearchController.java 파일을 찾아 호출부 및 파라미터 변환 로직을 확인하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# SearchController.java 파일 위치 확인
rg -g 'SearchController.java' -l src/main/java

# SearchController.java 상단 내용(임포트·엔드포인트) 출력
rg -g 'SearchController.java' -l src/main/java | xargs -I {} sed -n '1,200p' {}

Length of output: 128


🏁 Script executed:

#!/bin/bash
# Find any controllers or classes related to "Search" in the adapter layer
rg -g '*Search*.java' -l src/main/java/site/kikihi/custom/platform/adapter/in/web

# Print top lines of each found Search*.java to inspect pagination mapping
rg -g '*Search*.java' -l src/main/java/site/kikihi/custom/platform/adapter/in/web | xargs -I {} sed -n '1,200p' {}

# Search for PageRequest.of usage across all controllers to compare handling
rg -nP --max-columns=200 -C2 'PageRequest\.of\(' src/main/java/site/kikihi/custom/platform/adapter/in/web

Length of output: 2145


🏁 Script executed:

#!/bin/bash
# SearchUseCase interface reference locations
rg -nP 'interface\s+SearchUseCase' -n src/main/java

# Calls to searchProducts(...) throughout the codebase
rg -nP 'searchProducts\s*\(' -n src/main/java

# Check for any REST endpoints containing "search"
rg -nP '@GetMapping.*search' -n src/main/java
rg -nP '@RequestMapping.*search' -n src/main/java

Length of output: 1286


🏁 Script executed:

#!/bin/bash
# Find and print SearchController to inspect pageable mapping
FILE=$(rg -g 'SearchController.java' -l src/main/java)
if [ -z "$FILE" ]; then
  echo "SearchController.java not found"
  exit 1
fi
echo "=== $FILE ==="
sed -n '1,200p' "$FILE"

Length of output: 153


SearchController.java에서 페이지 인덱스를 어떻게 처리하는지 확인하기 위해 아래 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
# SearchController.java 상단 및 페이지 파라미터 매핑 로직 출력
sed -n '1,200p' src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java

# searchProducts 호출부와 파라미터 전달부 확인
grep -nP --max-columns=200 -C3 'searchProducts' src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java

Length of output: 4204


SearchService: custom PageRequest 1-base → Spring Data 0-base 변환 적용 필요

현재 global PageRequest.page는 기본값이 1이며(@Builder.Default private int page = 1;, @Schema에도 “페이지 번호 (1부터 시작)”으로 명시) 사용자가 1을 넘겨주면 pageRequest.getPage()가 1을 반환합니다.
그러나 SearchService.java 59번째 줄에서 Spring Data의 PageRequest.of(page, size)에 직접 전달하여 0-base로 변환하지 않아 첫 페이지(인덱스 0)가 누락됩니다.

수정 제안:

- .withPageable(PageRequest.of(page, size)) // 1-base page 전달 → 누락 문제
+ .withPageable(PageRequest.of(page - 1, size)) // 1-base → 0-base 변환 적용

page - 1 결과가 0 미만이 될 수 있으므로, 잘못된 파라미터 입력 시 예외 처리(IllegalArgumentException 등) 또는 기본값(1) 보정 로직을 추가 검토해 주세요.
• ProductController에서는 이미 pageRequest.getPage() - 1 처리를 하고 있으나, SearchController → SearchService 경로에는 변환 로직이 빠져 있습니다. 해당 경로 전체를 점검해 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size)
.withMinScore(minScore) // <<-- 추가됨
.build();
.withPageable(PageRequest.of(page - 1, size)) // 1-base → 0-base 변환 적용
.withMinScore(minScore) // <<-- 추가됨
.build();

Comment on lines +100 to +116
@Override
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());
}

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

무단 삭제 검출이 불가한 흐름 — 특정 예외로 한정 처리 혹은 어댑터 예외 위임

현재 레포지토리가 예외를 던지지 않으면 try 블록이 그대로 통과하여 항상 성공처럼 보입니다. 앞서 어댑터에서 0건 삭제 시 EmptyResultDataAccessException을 던지도록 변경했다는 가정하에, 여기서는 해당 예외만 받아 도메인 에러로 매핑하는 편이 명확합니다. 범용 Exception 캐치는 지양합니다.

-        try {
-            port.deleteSearch(search.getId(), user.getId());
-        } catch (Exception e) {
-            throw new IllegalArgumentException(ErrorCode.UNAUTHORIZED_DELETE_SEARCH.getMessage());
-        }
+        try {
+            port.deleteSearch(search.getId(), user.getId());
+        } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+            throw new IllegalArgumentException(ErrorCode.UNAUTHORIZED_DELETE_SEARCH.getMessage());
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
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());
}
}
@Override
public void deleteMySearchKeyword(Long searchId, UUID userId) {
/// 유저 예외 처리
User user = getUser(userId);
/// 검색 기록 예외 처리
Search search = getSearch(searchId);
/// 유저와 특정 키워드를 바탕으로 삭제합니다.
try {
port.deleteSearch(search.getId(), user.getId());
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
throw new IllegalArgumentException(ErrorCode.UNAUTHORIZED_DELETE_SEARCH.getMessage());
}
}
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/application/service/SearchService.java
around lines 100 to 116, the method currently catches broad Exception which
hides real failures and makes unauthorized-delete detection impossible; change
the try/catch to only catch the specific adapter exception thrown when no rows
are deleted (e.g. EmptyResultDataAccessException or the adapter-specific
exception) and map that to the domain IllegalArgumentException with
ErrorCode.UNAUTHORIZED_DELETE_SEARCH.getMessage(), while letting other
exceptions propagate (or wrap them in a more appropriate higher-level exception)
so real errors aren’t swallowed.

@doup2001 doup2001 merged commit 99f8400 into develop Aug 21, 2025
2 checks passed
Copy link
Contributor

@hwangjiyoung02 hwangjiyoung02 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security까지 일괄적으로 수정해주셨네요!! ElasticSearch 검색할때 DSL 작성하는 법 알아갑니다ㅎㅎ.! 수고하셨습니다~

boolean checkSearch(UUID userId);

/// 나의 최근 검색어 저장 끄기
void turnOffMySearchKeyword(UUID userId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 기능은 프론트상에서 못본거 같은데 최근 검색어 저장을 끄는 부분은 어디에 있나여?

public List<Product> 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 테스트 하셨을때 키보드 원하는 키워드에 대해 잘 검색되셨을까요?? 키보드 같은 경우 options에 스위치 축에 대한 정보가 많이 나와있어서,, test 후에 options 필드도 추가하면 좋을 것 같습니다:)

)._toQuery();

// NativeQuery
NativeQuery query = NativeQuery.builder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ElasticSearch로 검색할 때 Criteria wrapper만 쓰는 줄 알았는데 Elastic 5.xx 부터는 NativeQuery를 쓴다고 하네요! 배워갑니다 ㅎㅎ

@doup2001 doup2001 changed the title ✨ feat/product/KIKI-61-Search : 검색관련 기능 고도고화 ✨ feat/product/KIKI-61-Search : 검색관련 기능 고도화 Aug 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ feat : 검색 기능 고도화

3 participants