-
Notifications
You must be signed in to change notification settings - Fork 1
✨ feat/product/KIKI-61-Search : 검색관련 기능 고도화 #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 자동저장 여부 파악 - 취소/확정 로직 수정
Test Results34 tests 34 ✅ 6s ⏱️ Results for commit 3797aab. |
Walkthrough
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
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
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
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
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 unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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의 실제 컬럼명(createdAtvscreated_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: 오탈자: 메서드명deleteALlSearch→deleteAllSearch로 정정 권장
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: 오탈자 연쇄 수정:deleteALlSearch→deleteAllSearch앞서 어댑터/포트와 함께 메서드명을 정정해야 합니다.
- 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.
📒 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_SEARCH를IllegalArgumentException으로 throw- SearchService.java(185행):
ErrorCode.SEARCH_NOT_FOUND를NoSuchElementException으로 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:156src/main/java/site/kikihi/custom/platform/application/service/SearchService.java:168src/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: 메서드명 오타 수정 완료 및 반영 확인아래 파일들에서
deleteALlSearch가deleteAllSearches로 일관되게 변경된 것을 확인했습니다.
- 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 우려가 없습니다.
| on: | ||
| push: | ||
| branches: [ "develop","feat/product/KIKI-61-ElasticSearch"] | ||
| branches: [ "develop"] | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
PR/특정 브랜치에서 CI가 동작하지 않을 위험 — pull_request 트리거 추가 및 배포 단계 가드 권장
push 트리거를 develop으로 한정하면, 기능 브랜치/PR에서는 CI가 아예 실행되지 않아 품질 게이트가 비어 있게 됩니다. PR 기반 검증을 위해 pull_request 트리거를 추가하고, 배포/푸시는 push(develop)에서만 실행되도록 가드해 주세요.
아래처럼 트리거와 배포 단계 조건을 분리하는 것을 제안드립니다.
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.
| on: | |
| push: | |
| branches: [ "develop","feat/product/KIKI-61-ElasticSearch"] | |
| branches: [ "develop"] | |
| on: | |
| push: | |
| branches: [ "develop" ] | |
| pull_request: | |
| branches: [ "develop" ] | |
| workflow_dispatch: |
| 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.
| summary = "검색어 자동저장 기능 ON API", | ||
| description = "JWT를 바탕으로 자동저장을 킵니다." | ||
| ) | ||
| ApiResponse<Void> turnOnSearch( | ||
| @AuthenticationPrincipal PrincipalDetails principalDetails | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오타 수정: “킵니다” → “켭니다”
사용자 노출 문구이므로 맞춤법을 맞춰 주세요.
@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.
| 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.
| private boolean isSearch; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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/**/*.javaLength 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.javaLength 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.
| 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.
| @Override | ||
| public void deleteSearch(Long searchId, UUID userId) { | ||
| repository.deleteByIdAndUserId(searchId, userId); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
권한 검증이 실질적으로 동작하지 않을 수 있습니다 — 삭제 건수 확인 로직 추가 제안
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의 시그니처를 void → long으로 조정이 필요합니다. 원하시면 레포지토리/포트/서비스까지 일괄 변경 PR 패치도 드릴게요.
| /// 키워드 검색 (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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
빈/공백 키워드 처리 및 검색기록 저장 실패의 비차단화
- 공백 키워드는 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.
| /// 키워드 검색 (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.
| .withPageable(PageRequest.of(page, size)) //page-> from으로 자동 변환(from=page * size) | ||
| .withMinScore(minScore) // <<-- 추가됨 | ||
| .build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 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/globalLength 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.javaLength 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.javaLength 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/webLength 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/javaLength 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.javaLength 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.
| .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(); |
| @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()); | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
무단 삭제 검출이 불가한 흐름 — 특정 예외로 한정 처리 혹은 어댑터 예외 위임
현재 레포지토리가 예외를 던지지 않으면 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.
| @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.
hwangjiyoung02
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
security까지 일괄적으로 수정해주셨네요!! ElasticSearch 검색할때 DSL 작성하는 법 알아갑니다ㅎㅎ.! 수고하셨습니다~
| boolean checkSearch(UUID userId); | ||
|
|
||
| /// 나의 최근 검색어 저장 끄기 | ||
| void turnOffMySearchKeyword(UUID userId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 기능은 프론트상에서 못본거 같은데 최근 검색어 저장을 끄는 부분은 어디에 있나여?
| 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 테스트 하셨을때 키보드 원하는 키워드에 대해 잘 검색되셨을까요?? 키보드 같은 경우 options에 스위치 축에 대한 정보가 많이 나와있어서,, test 후에 options 필드도 추가하면 좋을 것 같습니다:)
| )._toQuery(); | ||
|
|
||
| // NativeQuery | ||
| NativeQuery query = NativeQuery.builder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ElasticSearch로 검색할 때 Criteria wrapper만 쓰는 줄 알았는데 Elastic 5.xx 부터는 NativeQuery를 쓴다고 하네요! 배워갑니다 ㅎㅎ
📌 작업한 내용
🔍 참고 사항
x
🖼️ 스크린샷
🔗 관련 이슈
#64
✅ 체크리스트
Summary by CodeRabbit