-
Notifications
You must be signed in to change notification settings - Fork 1
✨ feat/custom/KIKI-73-BE-추천 : 키보드 추천 튜토리얼, 유사상품 리스트 조회 구현 #67 #66
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
The head ref may contain hidden characters: "feat/custom/KIKI-73-BE-\uCD94\uCC9C-\uAE30\uB2A5-\uAD6C\uD604"
Conversation
- 아직 테스트가 더 필요함 - 주석 및 reasons는 추후 제거할 예정(테스트용)
- request를 데이터 검색 키워드로 변경하는 mapper 추가
Walkthrough
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant RC as RecommendationController
participant RS as RecommendationService
participant RKP as RecommendKeyboardPort<br/>(RecommendKeyboardAdapter)
participant DB as MongoDB
Note over U,RC: 튜토리얼 키보드 추천 (POST /tutorial)
U->>RC: KeyboardRecommendationRequest
RC->>RS: getTutorialKeyboardRecommendation(userId, request)
RS->>RKP: filterAndRecommendKeyboards(mapped options, price range)
RKP->>DB: Query by options/price
DB-->>RKP: ProductDocument[]
RKP-->>RS: List<Product>
RS-->>RC: List<KeyboardRecommendationResponse>
RC-->>U: ApiResponse<List<KeyboardRecommendationResponse>>
sequenceDiagram
autonumber
actor U as User
participant RC as RecommendationController
participant RS as RecommendationService
participant RKP as RecommendKeyboardPort<br/>(RecommendKeyboardAdapter)
participant DB as MongoDB
participant BM as BookmarkPort
Note over U,RC: 유사 상품 추천 (GET /{productId})
U->>RC: productId
RC->>RS: getSimilarProducts(userId, productId)
RS->>DB: loadProduct(productId)
DB-->>RS: Product
RS->>RKP: getSimilarProducts(userId, productId, baseProduct)
RKP->>DB: 후보 조회(카테고리/가격대)
RKP->>BM: 사용자 북마크 조회(선호 신호)
BM-->>RKP: 선호 데이터
RKP->>RKP: 유사도 스코어링 및 정렬
RKP-->>RS: 상위 N개 List<Product>
RS-->>RC: List<Product>
RC-->>U: ApiResponse<List<KeyboardRecommendationResponse>>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 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 (
|
Test Results34 tests 34 ✅ 6s ⏱️ Results for commit c72d9b7. ♻️ This comment has been updated with latest results. |
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: 10
🧹 Nitpick comments (34)
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java (2)
31-35: 상품 상세 조회의 식별자 전달 방식: PathVariable 전환 고려Line 31의
@RequestParam String id는 리소스 식별자 전달 관례상@PathVariable이 더 일관적입니다(캐싱/링크 공유/문서 가독성 측면). 구현체에서 이미 Path 기반 라우팅을 사용 중이라면 Spec도 맞춰주는 편이 좋습니다. 선택 사항이지만 추천드립니다.해당 위치만 수정하는 최소 diff:
- @RequestParam String id, + @PathVariable String id,추가로 필요한 import(선택, 인터페이스 상단에 적용):
import org.springframework.web.bind.annotation.PathVariable;
53-55: 카테고리 example 값 일관화 필요현재 Swagger 스펙에서 동일한
CategoryType파라미터에 대해 예시 값이 서로 다르게 설정되어 있어 혼동을 일으킬 수 있습니다. 실제 지원 카테고리 셋에 맞춰 하나의 예시 값으로 통일해 주세요.• src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/BookmarkControllerSpec.java (line 68)
@parameter(description = "카테고리", example = "keycap")
• src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java (line 53)
@parameter(description = "카테고리", required = true, example = "keyboard")– 실제
CategoryTypeenum에 정의된 값 중 하나로 통일하거나(예:"keyboard"), 프로젝트 전반에 걸쳐 공통으로 사용할 대표 카테고리 예시 하나를 선택해주세요.src/main/java/site/kikihi/custom/platform/application/in/recommendation/RecommendationUseCase.java (1)
14-19: UseCase 계층의 DTO 의존성 제거 검토UseCase 인터페이스(Line 15)가 웹 DTO(
KeyboardRecommendationResponse)를 반환하면서 계층 간 결합이 생겼습니다. 클린/헥사고날 아키텍처 관점에서 애플리케이션 규약은 도메인 모델을 반환하고, DTO 변환은 웹 어댑터에서 담당하는 것이 바람직합니다.권장 방향:
List<Product>또는 전용 도메인 VO 반환- 컨트롤러에서
KeyboardRecommendationResponse.from(domain)변환- 서비스/포트는 도메인에 집중
src/main/java/site/kikihi/custom/platform/application/out/recommend/RecommendKeyboardPort.java (4)
26-36: 파라미터 폭주 개선: 옵션 DTO 활용 및 타입 명확화
filterAndRecommendKeyboards시그니처가 다수의 원시 파라미터를 받습니다. 가독성·확장성·테스트 용이성을 위해 UI 옵션 DTO(KeyboardOptions)를 직접 사용하고, 가격은 선택값(Integer)으로, 흡음재/RGB는Boolean으로 의미를 명확히 하길 권장합니다. 데이터가"○"로 저장된 제약은 어댑터 내부 변환으로 캡슐화하세요.예시 최소 diff:
- List<Product> filterAndRecommendKeyboards( - UUID userId, - String size, - Integer keyPressure, - String layout, - List<String> switchType, - String soundDampener, - String rgb, - int minPrice, - int maxPrice - ); + List<Product> filterAndRecommendKeyboards( + UUID userId, + KeyboardOptions options, // size/layout/switchType/soundDampener/rgb 포함 + Integer keyPressure, // 별도 옵션인 경우만 유지 + Integer minPrice, // null 허용 → 범위 미적용 + Integer maxPrice // null 허용 → 범위 미적용 + );추가 제안:
- min/max의 포함/배제(>=, <=) 정책을 Javadoc에 명시
- 통화 단위(KRW)와 단위(원) 명시
46-47: 중복 파라미터 제거: productId vs baseProduct
getSimilarProducts(UUID userId, String productId, Product baseProduct)는 동일 식별 정보가 중복됩니다. 불일치 시 버그가 됩니다. 하나만 유지하세요.두 가지 대안 중 하나를 선택:
- 어댑터에서 로드:
List<Product> getSimilarProducts(UUID userId, String productId);- 상위에서 로드:
List<Product> getSimilarProducts(UUID userId, Product baseProduct);예시 diff(첫 번째 대안):
- List<Product> getSimilarProducts(UUID userId, String productId,Product baseProduct); + List<Product> getSimilarProducts(UUID userId, String productId);
12-25: Javadoc 오탈자 및 명세 보강
- Line 18: "키보드 배열의 크기 )" → 괄호 잔존
- 가격/키압 단위, 경계값 동작(포함/배제), 정렬 기준(유사도/가격/개인화 점수) 명시 권장
예시:
- keyPressure: cN 또는 g 단위? 허용 범위?
- minPrice/maxPrice: null 허용 여부, 음수 처리
- switchType: 허용 문자열 셋(document 링크)
9-47: RecommendPort 범용화 및 옵션 변환 책임 분리 제안현재 키보드 전용으로 설계된 Port/Adapter 구조가 유사상품(keycap, switch, housing 등) 지원에 제약을 주고 있으며, 옵션 처리 로직도 어댑터에 과도하게 분산되어 있습니다. 다음 영역을 중심으로 리팩토링을 권장드립니다.
• Port 인터페이스 추상화
– 파일:src/main/java/site/kikihi/custom/platform/application/out/recommend/RecommendKeyboardPort.java
– 현황:RecommendKeyboardPort이름 및 시그니처가 키보드 전용(예: size, keyPressure, layout, switchType, soundDampener, rgb)
– 제안:
•RecommendProductPort로 명칭 변경 후category파라미터(예: enum 또는 String)를 추가
• 또는 카테고리별 Port(·Adapter) 구현체 분리 + 전략 주입 방식으로 구조 재설계• Adapter 내부 ‘keyboard’ 하드코딩 제거
– 파일:src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java
– 현황:
java bookmarkPort.getBookmarksByUserIdAndCategoryId(userId, "keyboard");
– 제안: Port 계층에서 전달받은category값 사용• 메서드 시그니처 확장
– 대상:filterAndRecommendKeyboards,getSimilarProducts
– 제안:category인자를 모두 포함하도록 변경하여 유연한 상품 추천 흐름 확보• 옵션 매핑 책임 분리
– 현황: SoundDampener, RGB 옵션을 String("○"/null)으로 포트까지 전달 후 어댑터에서 직접 쿼리 생성
– 제안:
• Port 계층 시그니처에서boolean또는 enum 타입으로 옵션 수신
• 어댑터에서만 “○”/“X” 포맷으로 변환하여 DB 쿼리 기준에 맞게 매핑src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/RecommendControllerSpec.java (3)
39-42: 스웨거에서 Principal 숨김 처리 누락API 문서 노출 시 인증 주체는 숨기는 게 일관적입니다.
BookmarkControllerSpec와 동일하게@Parameter(hidden = true)를 추가해 주세요.- ApiResponse<List<KeyboardRecommendationResponse>> getTutorialKeyboardRecommendation( - @AuthenticationPrincipal PrincipalDetails principalDetails, + ApiResponse<List<KeyboardRecommendationResponse>> getTutorialKeyboardRecommendation( + @io.swagger.v3.oas.annotations.Parameter(hidden = true) + @AuthenticationPrincipal PrincipalDetails principalDetails, @Valid @RequestBody KeyboardRecommendationRequest request );또한 상단 import에
io.swagger.v3.oas.annotations.Parameter가 없다면 추가가 필요합니다.
51-54: productId 예시와 Principal 숨김 처리 제안
- 문서 가독성을 위해
productId에 예시를 달아 주세요.- Principal도 숨김 처리 일관화가 필요합니다.
- ApiResponse<List<KeyboardRecommendationResponse>> getSimilarProducts( - @PathVariable("productId") String productId, - @AuthenticationPrincipal PrincipalDetails principalDetails + ApiResponse<List<KeyboardRecommendationResponse>> getSimilarProducts( + @io.swagger.v3.oas.annotations.Parameter(example = "665a3df0c2a14b1fb09e3a1b") + @PathVariable("productId") String productId, + @io.swagger.v3.oas.annotations.Parameter(hidden = true) + @AuthenticationPrincipal PrincipalDetails principalDetails );
51-54: 유사 상품 응답 DTO의 범용성
getSimilarProducts가 키캡/스위치/하우징에도 쓰인다면, 키보드 전용 DTO(KeyboardRecommendationResponse)보다 범용ProductListResponse를 고려해 보세요. 카테고리별 확장성/일관성 측면에서 유리합니다.src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/KeyboardOptionsConverter.java (3)
21-50: 빈 리스트 반환 시 필터 무효화 이슈 가능성
mapSwitchTypeToOptionNames(null)이 빈 리스트를 반환합니다. 어댑터/포트에서$in []형태로 처리되면 결과가 0건이 될 수 있습니다. 소비 측(서비스)에서 빈 리스트를null로 치환하거나, 여기서null을 반환하도록 합의가 필요합니다.
26-47: 옵션명 불일치/동의어 처리 제안
- "clicky" 영문 소문자만 포함되어 있어 한글 표기(예: "클릭키", "클릭키축")가 누락입니다.
- 동일 축의 다양한 표기(공백/대소문/버전 표기, 예: "잉크축V2", "잉크 V2")가 데이터에 존재할 수 있습니다.
데이터 정규화가 어렵다면, 동의어 리스트(케이스 인센서티브 포함)를 확장하거나 어댑터에서 정규식/케이스 무시 매칭을 고려해 주세요.
61-68: 키압 매핑(49/50) 정확도 재검증LIGHT=49, NORMAL=50로 고정 매핑했습니다. 데이터가 범위(예: 45~50g) 혹은 다른 기준(예: 45/48/50)으로 저장되어 있다면 매칭률이 급감합니다. 가능하면 범위 매칭(>=, <=)이나 후보 다중 값 매칭을 어댑터에서 지원하는 방향을 권장합니다.
src/main/java/site/kikihi/custom/platform/application/service/RecommendationService.java (3)
103-107: 비로그인 사용자에게 빈 리스트 반환 — 의도 확인 필요튜토리얼 추천에서
userId == null이면 빈 리스트를 반환합니다. 튜토리얼 특성상 비로그인 사용자도 이용 가능해야 할 가능성이 높습니다. 비로그인도 조회 가능하도록 허용하거나, PR 설명/요구사항과 정합성 확인이 필요합니다.가능한 변경 예시:
- public List<KeyboardRecommendationResponse> getTutorialKeyboardRecommendation(UUID userId, KeyboardRecommendationRequest request) { - if (userId == null) { - return Collections.emptyList(); - } + public List<KeyboardRecommendationResponse> getTutorialKeyboardRecommendation(UUID userId, KeyboardRecommendationRequest request) { List<Product> documents=recommendKeyboardPort.filterAndRecommendKeyboards( ... ); return documents.stream().map(KeyboardRecommendationResponse :: from).toList(); }
95-101: 읽기 전용 트랜잭션 선언 제안읽기 전용 서비스 메서드에
@Transactional(readOnly = true)를 부여하면 불필요한 쓰기 컨텍스트 오버헤드를 줄일 수 있습니다.- @Override - public List<KeyboardRecommendationResponse> getTutorialKeyboardRecommendation(UUID userId, KeyboardRecommendationRequest request) { + @Override + @org.springframework.transaction.annotation.Transactional(readOnly = true) + public List<KeyboardRecommendationResponse> getTutorialKeyboardRecommendation(UUID userId, KeyboardRecommendationRequest request) { ... } - @Override - public List<Product> getSimilarProducts(UUID userId, String productId) { + @Override + @org.springframework.transaction.annotation.Transactional(readOnly = true) + public List<Product> getSimilarProducts(UUID userId, String productId) { ... }Also applies to: 132-151
283-315: 가중치 샘플링 알고리즘의 비효율성(성능 최적화 여지)
pool.removeIf(id -> id.equals(pid))가 매 선택마다 O(N)으로 동작합니다. 북마크 수가 많은 경우 비효율적입니다. 카운트 누적 기반의 누적합 배열(프리픽스 합)로 한 번에 K개 샘플링하거나, 가중치 힙/저장소를 활용하는 방식으로 개선 가능.src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/KeyboardRecommendationResponse.java (2)
20-22: 스키마 이름/주석 불일치클래스 이름이
KeyboardRecommendationResponse인데 스키마 이름은 "KeyboardRecommendationListResponse"이며, 주석은 "상품 상세 응답 DTO"로 되어 있습니다. 문서 혼란을 줄이기 위해 정리해 주세요.-@Schema(name = "KeyboardRecommendationListResponse", description = "튜토리얼 키보드 추천 리스트 응답") +@Schema(name = "KeyboardRecommendationResponse", description = "튜토리얼/유사 상품 추천 리스트 응답")
58-63: 리스트 변환 메서드 네이밍/중복에 대한 사소한 개선정적 팩토리
from(List<Product>)는 기존 다른 DTO들과 일관적이지만, 가독성을 위해listFrom(products)같은 구분된 이름을 고려할 수 있습니다. 선택 사항입니다.src/main/java/site/kikihi/custom/platform/adapter/in/web/RecommendationController.java (2)
50-64: POST 소비 미디어 타입 명시 및 게스트 사용자 동작 확인
@PostMapping("/tutorial")에consumes = MediaType.APPLICATION_JSON_VALUE를 명시하면 클라이언트/문서 일관성이 좋아집니다.- 현재 서비스는 비로그인 시 빈 리스트를 반환합니다. 튜토리얼 요구사항상 게스트 허용 여부를 확인해 주세요.
-import jakarta.validation.Valid; +import jakarta.validation.Valid; +import org.springframework.http.MediaType; ... - @PostMapping("/tutorial") + @PostMapping(value = "/tutorial", consumes = MediaType.APPLICATION_JSON_VALUE)
70-83: 유사상품: 키캡/스위치/하우징 0건 이슈 진단 가이드현재 컨트롤러는 서비스 반환값을 그대로 매핑합니다. 0건 문제는 상위 레이어(서비스/어댑터/쿼리)에서의 필터 조건 불일치 가능성이 큽니다. 다음을 점검해 주세요.
RecommendKeyboardPort.getSimilarProducts(...)에서 카테고리별 필터가 키보드에만 적용되어 있지 않은지.- 데이터에 "○"로 저장된 필드에 정확히 "○"가 전달되는지(
soundDampener,rgb등).- 스위치 타입이 비선택(null)일 때
$in []가 되지 않도록 null 처리되는지(서비스 수정 제안 반영).원하시면 Mongo 쿼리/어댑터 레이어 로그 주입 패치를 드리겠습니다.
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardRecommendationRequest.java (4)
12-13: OpenAPI 예시값 불일치: size 예시를 실제 enum 값과 맞추세요.
example = "ten"은 실제 직렬화 값("tenkeyless")과 달라 혼선을 유발합니다. API 문서/스웨거에서 잘못된 값으로 시도하게 됩니다.적용 diff:
- @Schema(description = "키보드 배열(Size) 옵션", example = "ten", required = true) + @Schema(description = "키보드 배열(Size) 옵션", example = "tenkeyless", required = true)
18-19: 오타/일관성: layout 예시 "egonomic" 확인 필요Enum의 직렬화 값이 "egonomic"(오타)로 설계되어 있지만, 일반적으로 "ergonomic"이 맞습니다. 데이터 소스/프론트와 합의된 표준 철자 여부를 확인해 주세요. 만약 "egonomic"이 데이터 표준이라면 문서와 컨버터, 어댑터 비교 로직이 모두 그 값을 일관되게 사용해야 합니다.
원하시면 전체 코드베이스에서 "egonomic" 사용 현황을 스캔하는 스크립트를 드리겠습니다.
30-34: 가격 경계값 처리 강건성: primitive → wrapper, Bean Validation 추가 권장현재 0을 "미설정" 의미로 사용합니다. 이를 명시적으로 표현하려면
int대신Integer를 쓰고, 유효성 검사를 추가하는 편이 안전합니다. 또한maxPrice >= minPrice보장도 필요합니다.예시 diff:
-import lombok.Getter; -import lombok.Setter; +import lombok.Getter; +import lombok.Setter; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; @@ - private int minPrice; + @PositiveOrZero + private Integer minPrice; @@ - private int maxPrice; + @PositiveOrZero + private Integer maxPrice;추가로 서비스 단(or 컨트롤러 바인딩 검증)에서
maxPrice != null && minPrice != null && maxPrice < minPrice인 경우 400을 주도록 검증 로직을 넣는 것을 권장합니다.
7-29: 요청 DTO 검증 누락: 필수 옵션에 @NotNull 추가 권장스웨거
required = true만으로는 런타임 검증이 되지 않습니다. Bean Validation으로 보완하세요.예시 diff:
@@ - private KeyboardOptions.Size size; + @NotNull + private KeyboardOptions.Size size; @@ - private KeyboardOptions.KeyPressure keyPressure; + @NotNull + private KeyboardOptions.KeyPressure keyPressure; @@ - private KeyboardOptions.Layout layout; + @NotNull + private KeyboardOptions.Layout layout; @@ - private KeyboardOptions.SwitchType switchType; + @NotNull + private KeyboardOptions.SwitchType switchType; @@ - private KeyboardOptions.SoundDampener soundDampener; + @NotNull + private KeyboardOptions.SoundDampener soundDampener; @@ - private KeyboardOptions.RGB rgb; + @NotNull + private KeyboardOptions.RGB rgb;src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardOptions.java (2)
8-14: 불필요한 Lombok 사용외부 클래스
KeyboardOptions는 필드가 없어@Getter,@RequiredArgsConstructor가 의미 없습니다. 제거해도 됩니다.적용 diff:
-@Getter -@RequiredArgsConstructor @Schema( name = "[요청][상품] 키보드 옵션 Enum", description = "키보드 추천 서비스에서 사용하는 키보드 옵션 Enum입니다." ) public class KeyboardOptions {
59-61: 철자 표준화 제언: "egonomic" vs "ergonomic"데이터 표준이 "egonomic"이라면 유지하되, 외부 입력으로 "ergonomic"도 수용하도록
fromValue에서 동의어 매핑을 추가하는 것을 권장합니다.원하시면
fromValue내에 아래와 같은 매핑을 제안드립니다:String norm = value.trim().toLowerCase(); if ("ergonomic".equals(norm)) norm = "egonomic";src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java (8)
27-31: 미사용 필드 정리
repository,mapper는 사용되지 않습니다. 혼선을 줄이기 위해 제거하세요.적용 diff:
- private final ProductDocumentRepository repository; @@ - private static final ObjectMapper mapper = new ObjectMapper();
51-54: Size 필터의 검색어 정합성
description에서 단순히size문자열을 정규식으로 찾는 것은 데이터(예: "텐키리스", "풀배열", "미니")와 불일치할 수 있습니다. 컨버터에서 한글 키워드로 이미 매핑한다면 OK, 아니라면 영문값→한글 키워드 매핑(또는 spec 기반 필터)로 보강하세요.필요 시,
"tenkeyless" -> "텐키리스","full" -> "풀배열","mini" -> "미니"매핑 후orOperator로 description/spec을 함께 탐색하는 방식을 권장합니다.
56-64: 키압 정규식 경계 처리
"^([0-4]?[0-9])g$"는 "0g"~"49g"만 허용합니다. "45 g", "45gf" 등 변형 표기가 있으면 미스합니다.- NORMAL 정규식은 "100g 이상"도 포함하지만 "50 g" 앞뒤 공백/단위 변형을 처리하지 않습니다.
보다 관대한 정규식을 권장합니다.
예시 diff:
- if (keyPressure <= 49) { // LIGHT 구간 - query.addCriteria(Criteria.where(keyPressureField).regex("^([0-4]?[0-9])g$")); - } else { // NORMAL 구간 - query.addCriteria(Criteria.where(keyPressureField).regex("^([5-9][0-9]|[1-9][0-9]{2,})g$")); - } + String light = "^(\\s*[0-4]?\\d\\s*)(g|gf)?\\s*$"; + String normal = "^(\\s*(?:[5-9]\\d|\\d{3,})\\s*)(g|gf)?\\s*$"; + query.addCriteria(Criteria.where(keyPressureField).regex( + keyPressure <= 49 ? light : normal + ));
125-131: AND 조건 구성 방식 일관화일부 조건은
query.addCriteria(...), 일부는andCriterias로 모았다가 마지막에 AND로 적용합니다. 유지보수 관점에서 한 방식으로 통일하는 것을 권장합니다(예: 모두andCriterias에 모으기).
130-133: 로그 레벨 과다(INFO에서 대용량 객체 덤프)
- 쿼리 자체는 INFO로 괜찮지만,
baseSpec/candSpec,options, 상세 순위 로그 등은 DEBUG로 내리는 게 운영 로그 노이즈/비용을 줄입니다.- 대용량 맵/리스트를 INFO로 남기면 성능에도 영향이 있습니다.
예시 diff:
- log.info("baseSpec={}, candSpec={}", baseSpec, candSpec); - log.info("baseOptions={}, candOptions={}", baseOptions, candOptions); - log.info("baseDescriptions={}, candDescriptions={}", baseDescriptions, candDescriptions); + log.debug("baseSpec={}, candSpec={}", baseSpec, candSpec); + log.debug("baseOptions={}, candOptions={}", baseOptions, candOptions); + log.debug("baseDescriptions={}, candDescriptions={}", baseDescriptions, candDescriptions); @@ - log.info("===== 최종 추천 10개 상품 상세 ====="); + log.debug("===== 최종 추천 10개 상품 상세 ====="); @@ - log.info("ScoredProduct 생성: 상품ID={}, 점수={}, 이유={}", product.getId(), score, String.join(", ", reasons)); + log.debug("ScoredProduct 생성: 상품ID={}, 점수={}, 이유={}", product.getId(), score, String.join(", ", reasons));Also applies to: 241-244, 203-216, 447-448
283-292: 사이즈 키워드 탐색 로직 개선: equals → contains설명 문자열이 정확히 "풀배열"과 같지 않고 문장 내에 포함될 가능성이 큽니다.
contains또는 정규식 매칭으로 완화하세요.적용 diff:
- if (desc.trim().equalsIgnoreCase(size)) { + if (desc != null && desc.toLowerCase().contains(size.toLowerCase())) { return size; }
422-430: 스위치 계열 판정 로직이 과도하게 단순첫 글자 비교는 "blue/brown/black" 모두 'b'로 동일 취급되는 등 오탐이 많습니다. 제조사/계열(예: 체리/가테/정축/저소음 등) 기반 룰셋이나 매핑 테이블을 도입하는 것을 권장합니다.
간단 대안: 접두어 사전(
"cherry-","gateron-","kaihl-","silent-") 기반 비교 또는 레벤슈타인 유사도(성능 고려 시 캐싱).
157-160: 카테고리 하드코딩 문자열
"keyboard"문자열은 상수/enum로 중앙집중화해 오타를 방지하고 재사용성을 높이세요.
📜 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 (13)
build.gradle(1 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/RecommendationController.java(2 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/KeyboardOptionsConverter.java(1 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardOptions.java(1 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardRecommendationRequest.java(1 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/KeyboardRecommendationResponse.java(1 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java(2 hunks)src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/RecommendControllerSpec.java(2 hunks)src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java(1 hunks)src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java(2 hunks)src/main/java/site/kikihi/custom/platform/application/in/recommendation/RecommendationUseCase.java(2 hunks)src/main/java/site/kikihi/custom/platform/application/out/recommend/RecommendKeyboardPort.java(1 hunks)src/main/java/site/kikihi/custom/platform/application/service/RecommendationService.java(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java (1)
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/BookmarkControllerSpec.java (1)
Tag(23-94)
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/RecommendControllerSpec.java (2)
src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/SearchControllerSpec.java (4)
Operation(20-29)Operation(61-67)Operation(70-76)Operation(32-38)src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/BookmarkControllerSpec.java (1)
Operation(33-49)
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardRecommendationRequest.java (1)
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardOptions.java (1)
Getter(8-135)
src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java (2)
src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (3)
Getter(22-99)toDomain(84-98)from(65-81)src/test/java/site/kikihi/custom/platform/domain/product/ProductFixtures.java (4)
createProduct(34-56)fakeProduct(10-32)ProductFixtures(7-107)createProduct(82-104)
src/main/java/site/kikihi/custom/platform/application/out/recommend/RecommendKeyboardPort.java (2)
src/main/java/site/kikihi/custom/platform/application/in/search/SearchUseCase.java (2)
searchProducts(19-19)SearchUseCase(16-40)src/main/java/site/kikihi/custom/platform/application/out/user/UserPort.java (1)
UserPort(9-26)
src/main/java/site/kikihi/custom/platform/adapter/in/web/RecommendationController.java (1)
src/main/java/site/kikihi/custom/platform/adapter/in/web/SearchController.java (3)
GetMapping(27-45)RestController(19-130)PutMapping(104-115)
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/KeyboardRecommendationResponse.java (2)
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductListResponse.java (4)
Builder(24-91)from(51-61)product(73-75)from(80-90)src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductDetailResponse.java (4)
Builder(25-147)Builder(95-144)from(63-76)from(79-92)
src/main/java/site/kikihi/custom/platform/application/service/RecommendationService.java (1)
src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/KeyboardOptionsConverter.java (1)
KeyboardOptionsConverter(9-69)
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardOptions.java (1)
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardRecommendationRequest.java (1)
Getter(7-35)
⏰ 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 (2)
src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java (1)
65-65: 테스트 코드에서ProductDocument.builder(…)호출이나ProductFixtures클래스, 그리고specTable사용 흔적이 전혀 확인되지 않습니다. 아래 명령어를 실행해 수동으로 한 번 더 검증해 주세요.# 1. ProductDocument.builder() → .build() 호출 검색 rg -nP 'ProductDocument\.builder\(\)[\s\S]*?\.build\(\)' src/test # 2. ProductFixtures 클래스 검색 rg -nP 'class\s+ProductFixtures\b' src/test # 3. specTable 키워드 검색 rg -n 'specTable' src/test위 명령어에도 아무 결과가 없다면, 테스트 픽스처(
ProductFixtures)가 존재하지 않거나specTable기본값이 설정된 곳이 없는 상태입니다. 해당 부분을 수동으로 점검하신 뒤, 필요한 경우 테스트 픽스처를 추가하거나 기존 픽스처에specTable기본값을 세팅해 주세요.src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/KeyboardOptionsConverter.java (1)
53-59: 레이아웃 한글 표기 검증 필요"스텝스컬쳐2" vs "스텝스컬처2", "로우프로파일(LP)" vs "로우 프로파일(LP)" 등 데이터 표기와의 미묘한 차이로 매칭 실패 가능성이 있습니다. 실제
specTable내 저장 문자열과 정확히 일치하는지 확인이 필요합니다.
| } | ||
|
|
||
| group = 'com.jiyoung.kikihi' | ||
| group = 'site.kikihi.custom' |
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
그룹 ID 변경 영향도 점검 및 잔존 패키지 네임스페이스 탐지 제안
artifact 좌표가 com.jiyoung.kikihi → site.kikihi.custom 로 변경되었습니다. 배포 파이프라인(예: Artifactory/GitHub Packages), 문서화, 그리고 소스 트리의 최상위 패키지 네임스페이스가 모두 동기화되었는지 확인해주세요. 특히 기존 패키지 선언이 남아 있으면 런타임/리플렉션 스캔 이슈가 날 수 있습니다.
다음 스크립트로 잔존 네임스페이스와 스프링 부트/DM 플러그인 버전 호환성을 빠르게 점검할 수 있습니다.
필요하시다면 배포 파이프라인과 README의 좌표 업데이트 PR도 같이 준비해드릴게요.
🏁 Script executed:
#!/bin/bash
# 1) 과거 그룹/패키지 네임스페이스 잔존 여부 탐지
rg -nP -C2 'package\s+com\.jiyoung\.kikihi\b' || true
rg -n -C2 'com\.jiyoung\.kikihi' || true
# 2) 멀티모듈이 아닌 경우이지만, settings.gradle 등에 그룹 참조가 남아있는지 확인
fd -a settings.gradle | xargs -I{} sed -n '1,120p' {}
# 3) 부트/DM 플러그인 버전 문자열 노출(호환성 수동 검토용)
rg -n "org.springframework.boot' version" build.gradle
rg -n "io.spring.dependency-management' version" build.gradleLength of output: 1494
그룹 ID 변경 후 잔존 패키지 네임스페이스 수동 수정 필요
- src/main/java/site/kikihi/custom/global/logging/ControllerLoggingAspect.java (24행):
@pointcut에 남아있는com.jiyoung.kikihi.platform.adapter.in.web→site.kikihi.custom.platform.adapter.in.web으로 수정 - src/main/java/site/kikihi/custom/global/logging/LoggingAspect.java (16행):
@pointcut에 남아있는com.jiyoung.kikihi.platform.application.service→site.kikihi.custom.platform.application.service으로 수정
위 2개 파일 외에 package com.jiyoung.kikihi 선언은 확인되지 않았으며, settings.gradle에도 과거 그룹 참조가 없습니다.
build.gradle의 플러그인 버전(org.springframework.boot 3.4.3, io.spring.dependency-management 1.1.7)은 현행 버전으로, 별도 조치는 불필요합니다.
필요하시면 배포 파이프라인·README 문서의 좌표 업데이트도 함께 지원해드리겠습니다.
# ControllerLoggingAspect.java
- @Pointcut("execution(* com.jiyoung.kikihi.platform.adapter.in.web..*.*(..))")
+ @Pointcut("execution(* site.kikihi.custom.platform.adapter.in.web..*.*(..))")
# LoggingAspect.java
- @Pointcut("execution(* com.jiyoung.kikihi.platform.application.service.*Service.*(..))")
+ @Pointcut("execution(* site.kikihi.custom.platform.application.service.*Service.*(..))")🤖 Prompt for AI Agents
In build.gradle around line 7, after changing the group ID to
'site.kikihi.custom' you must update leftover package pointcut strings in two
Java files: open
src/main/java/site/kikihi/custom/global/logging/ControllerLoggingAspect.java and
on line 24 change the @Pointcut value from
'com.jiyoung.kikihi.platform.adapter.in.web' to
'site.kikihi.custom.platform.adapter.in.web'; then open
src/main/java/site/kikihi/custom/global/logging/LoggingAspect.java and on line
16 change the @Pointcut value from
'com.jiyoung.kikihi.platform.application.service' to
'site.kikihi.custom.platform.application.service'. Ensure you only replace those
string literals and keep imports/package declarations intact, then run a quick
build to verify no remaining references to com.jiyoung.kikihi remain.
| @Getter | ||
| @RequiredArgsConstructor | ||
| @Schema(name = "[요청][상품] 배열(Size) Enum", description = "키보드 배열 옵션") | ||
| public enum Size { | ||
| @Schema(description = "텐키리스 배열", example = "tenkeyless") | ||
| TENKEYLESS("tenkeyless"), | ||
|
|
||
| @Schema(description = "풀배열", example = "full") | ||
| FULL("full"), | ||
|
|
||
| @Schema(description = "미니 배열", example = "mini") | ||
| MINI("mini"); | ||
|
|
||
| private final String value; | ||
|
|
||
| @JsonValue | ||
| public String getValue() { | ||
| return value; | ||
| } | ||
| } |
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.
Enum 역직렬화 실패 가능성: @JsonCreator 추가
현재 @JsonValue만 있어 직렬화는 값으로 되지만, 역직렬화는 기본적으로 enum 이름(예: ERGONOMIC, YES)만 허용됩니다. 문서 예시는 "egonomic", "○", "light" 등 값 기반이므로 요청 바인딩이 실패합니다. 각 enum에 @JsonCreator(mode = DELEGATING) 팩토리를 추가하세요.
예시 diff(Size 하나만 예시, 동일 방식으로 모든 enum에 추가 필요):
public enum Size {
@@
private final String value;
@JsonValue
public String getValue() {
return value;
}
+
+ @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
+ public static Size fromValue(String value) {
+ if (value == null) return null;
+ for (Size v : values()) {
+ if (v.value.equalsIgnoreCase(value) || v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ throw new IllegalArgumentException("Unknown Size: " + value);
+ }
}SoundDampener/RGB는 "○"|"X" 이외에 "o"|"x", "O"|"X" 입력도 허용하려면 정규화 로직을 추가하세요.
필요한 import:
+import com.fasterxml.jackson.annotation.JsonCreator;Also applies to: 37-53, 55-71, 73-96, 98-114, 116-132
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardOptions.java
around lines 16 to 35 (and likewise for the other enum blocks at 37-53, 55-71,
73-96, 98-114, 116-132), the enums expose @JsonValue for serialization but lack
a @JsonCreator factory so deserialization from the string values (e.g.,
"tenkeyless", "egonomic", "○", "light") will fail; add a static factory
annotated with @JsonCreator(mode = DELEGATING) that accepts a String, normalizes
the input (trim, toLowerCase, map alternative symbols/characters for
SoundDampener and RGB, and accept 'o','O','x','X' variants), then returns the
matching enum by comparing against the enum's value field or known aliases, and
throw a clear IllegalArgumentException for unknown values; apply the same
pattern to all enums mentioned so incoming JSON binds by value instead of enum
name.
| @Schema(name = "[요청][상품] 키압(Key Pressure) Enum", description = "키압 옵션") | ||
| public enum Layout { | ||
| @Schema(description = "인체공학적 (스텝스컬쳐2)", example = "egonomic") | ||
| ERGONOMIC("egonomic"), | ||
|
|
||
| @Schema(description = "심플하고 깔끔한 (low 프로파일)", example = "simple") | ||
| SIMPLE("simple"); |
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.
스키마 설명 라벨 오류
Layout enum의 스키마 설명이 "키압(Key Pressure) Enum"으로 복붙된 듯합니다. 레이아웃으로 수정하세요.
적용 diff:
- @Schema(name = "[요청][상품] 키압(Key Pressure) Enum", description = "키압 옵션")
+ @Schema(name = "[요청][상품] 레이아웃(Layout) Enum", description = "레이아웃 옵션")
public enum Layout {📝 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.
| @Schema(name = "[요청][상품] 키압(Key Pressure) Enum", description = "키압 옵션") | |
| public enum Layout { | |
| @Schema(description = "인체공학적 (스텝스컬쳐2)", example = "egonomic") | |
| ERGONOMIC("egonomic"), | |
| @Schema(description = "심플하고 깔끔한 (low 프로파일)", example = "simple") | |
| SIMPLE("simple"); | |
| @Schema(name = "[요청][상품] 레이아웃(Layout) Enum", description = "레이아웃 옵션") | |
| public enum Layout { | |
| @Schema(description = "인체공학적 (스텝스컬쳐2)", example = "egonomic") | |
| ERGONOMIC("egonomic"), | |
| @Schema(description = "심플하고 깔끔한 (low 프로파일)", example = "simple") | |
| SIMPLE("simple"); |
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/product/KeyboardOptions.java
around lines 57 to 63, the @Schema name for the Layout enum incorrectly reads
"키압(Key Pressure) Enum"; change this to a correct label reflecting layout (e.g.,
"레이아웃(Layout) Enum" or "Layout Enum") and ensure the description/label text
references layout rather than key pressure so the schema accurately represents
the enum.
| @Schema(description = "정상가(원)", example = "599000.0") | ||
| double price, |
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.
price 필드 primitive(double)로 인한 NPE 위험
product.getPrice()가 null이면 오토 언박싱 시 NPE가 발생합니다. Double로 변경하거나, 안전한 디폴트를 적용해 주세요.
- @Schema(description = "정상가(원)", example = "599000.0")
- double price,
+ @Schema(description = "정상가(원)", example = "599000.0")
+ Double price,또는 from(Product)에서 안전 처리:
- .price(product.getPrice())
+ .price(product.getPrice() != null ? product.getPrice() : null) // null 허용Also applies to: 46-55
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/KeyboardRecommendationResponse.java
around lines 36-37 (and similarly lines 46-55), the price field is declared as
primitive double which will NPE on auto-unboxing if product.getPrice() is null;
change the field type to Double or, alternatively, keep it as double but ensure
from(Product) converts null to a safe default (e.g., 0.0) before assignment, and
apply the same null-safe handling for the other affected fields in lines 46-55.
| @Field("spec_table") | ||
| private Map<String, Object> specTable; // spec_table은 현재 비어있는 상태로 초기화 | ||
|
|
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
specTable NPE 예방: 기본값 초기화가 주석과 불일치합니다
Line 49–51에서 “비어있는 상태로 초기화”라고 되어 있으나 실제 필드는 기본값이 없습니다. null로 유지되면 이후 매퍼/컨버터(예: 추천 필터링에서 spec_table 접근)에서 NPE가 발생할 수 있습니다. 기본값을 부여하거나 toDomain에서 null 방어가 필요합니다.
권장 최소 diff:
- @Field("spec_table")
- private Map<String, Object> specTable; // spec_table은 현재 비어있는 상태로 초기화
+ @Field("spec_table")
+ @Builder.Default
+ private Map<String, Object> specTable = new java.util.HashMap<>(); // NPE 예방: 기본값toDomain에서도 이중 방어를 권장합니다(아래 별도 코멘트 참고).
📝 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.
| @Field("spec_table") | |
| private Map<String, Object> specTable; // spec_table은 현재 비어있는 상태로 초기화 | |
| @Field("spec_table") | |
| @Builder.Default | |
| private Map<String, Object> specTable = new java.util.HashMap<>(); // NPE 예방: 기본값 |
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java
around lines 49–51, the specTable field is documented as "initialized empty" but
has no default value causing possible NPEs; initialize specTable to an empty
mutable Map (e.g., new HashMap<>()) at declaration and also add a null-safe
fallback in toDomain (treat null as empty map) so callers and mappers never
observe null.
| if (layout != null && !layout.isBlank()) { | ||
| if ("ERGONOMIC".equalsIgnoreCase(layout)) { | ||
| query.addCriteria(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").is("○")); | ||
| } else if ("SIMPLE".equalsIgnoreCase(layout)) { | ||
| andCriterias.add(Criteria.where("spec_table.키보드구조 > 로우프로파일(LP)").is("○")); | ||
| andCriterias.add(Criteria.where("description").regex("스텝스컬쳐2", "i")); | ||
| andCriterias.add(Criteria.where("description").regex("lp", "i")); | ||
| } | ||
| } |
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.
Layout 조건 오류 및 일관성 문제
"SIMPLE"분기에서스텝스컬쳐2를 포함시키고 있어 의미가 충돌합니다(오히려 제외해야 자연스러움).- 앞선
"ERGONOMIC"은query.addCriteria를 쓰고,"SIMPLE"은andCriterias에만 추가하는 등 혼용되어 가독성이 떨어집니다. - 클라이언트가
"egonomic"(값) 또는"ERGONOMIC"(이름) 중 무엇을 전달하는지와도 불일치합니다.
다음처럼 수정 권장:
- if (layout != null && !layout.isBlank()) {
- if ("ERGONOMIC".equalsIgnoreCase(layout)) {
- query.addCriteria(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").is("○"));
- } else if ("SIMPLE".equalsIgnoreCase(layout)) {
- andCriterias.add(Criteria.where("spec_table.키보드구조 > 로우프로파일(LP)").is("○"));
- andCriterias.add(Criteria.where("description").regex("스텝스컬쳐2", "i"));
- andCriterias.add(Criteria.where("description").regex("lp", "i"));
- }
- }
+ if (layout != null && !layout.isBlank()) {
+ String val = layout.trim();
+ boolean ergonomic = "ERGONOMIC".equalsIgnoreCase(val) || "egonomic".equalsIgnoreCase(val);
+ if (ergonomic) {
+ andCriterias.add(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").is("○"));
+ } else if ("SIMPLE".equalsIgnoreCase(val)) {
+ andCriterias.add(Criteria.where("spec_table.키보드구조 > 로우프로파일(LP)").is("○"));
+ andCriterias.add(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").ne("○"));
+ }
+ }또한 가능한 한 모든 조건을 andCriterias에 담고 마지막에 한 번에 andOperator로 적용하는 일관된 스타일을 추천합니다.
📝 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.
| if (layout != null && !layout.isBlank()) { | |
| if ("ERGONOMIC".equalsIgnoreCase(layout)) { | |
| query.addCriteria(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").is("○")); | |
| } else if ("SIMPLE".equalsIgnoreCase(layout)) { | |
| andCriterias.add(Criteria.where("spec_table.키보드구조 > 로우프로파일(LP)").is("○")); | |
| andCriterias.add(Criteria.where("description").regex("스텝스컬쳐2", "i")); | |
| andCriterias.add(Criteria.where("description").regex("lp", "i")); | |
| } | |
| } | |
| if (layout != null && !layout.isBlank()) { | |
| String val = layout.trim(); | |
| boolean ergonomic = "ERGONOMIC".equalsIgnoreCase(val) || "egonomic".equalsIgnoreCase(val); | |
| if (ergonomic) { | |
| andCriterias.add(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").is("○")); | |
| } else if ("SIMPLE".equalsIgnoreCase(val)) { | |
| andCriterias.add(Criteria.where("spec_table.키보드구조 > 로우프로파일(LP)").is("○")); | |
| andCriterias.add(Criteria.where("spec_table.키보드구조 > 스텝스컬쳐2").ne("○")); | |
| } | |
| } |
🤖 Prompt for AI Agents
src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java
around lines 66-74: the SIMPLE branch incorrectly includes "스텝스컬쳐2" (should be
excluded) and the code mixes query.addCriteria with accumulating andCriterias
which reduces consistency; normalize the incoming layout by trimming and
uppercasing (e.g., layout = layout.trim().toUpperCase()), switch on that
normalized value, and for both ERGONOMIC and SIMPLE add Criteria objects into
andCriterias (do not call query.addCriteria inside the branch); for ERGONOMIC
add a Criteria matching spec_table.키보드구조 > 스텝스컬쳐2 == "○", for SIMPLE remove the
스텝스컬쳐2 positive match and instead add a negative/exclusion Criteria or ensure
description excludes 스텝스컬쳐2 (e.g.,
Criteria.where("description").not().regex("스텝스컬쳐2","i")), keep other SIMPLE
criteria (LP and lp regex) and finally apply all andCriterias with
query.addCriteria(new Criteria().andOperator(andCriterias.toArray(new
Criteria[0]))).
| // SoundDampener | ||
| if (soundDampener != null) { | ||
| if (soundDampener == KeyboardOptions.SoundDampener.YES.getValue()) { | ||
| andCriterias.add(new Criteria().orOperator( | ||
| Criteria.where("spec_table.키보드구조 > 흡음재").is("○"), | ||
| Criteria.where("description").regex("흡음재") | ||
| )); | ||
| } else { | ||
| query.addCriteria(new Criteria().andOperator( | ||
| Criteria.where("spec_table.키보드구조 > 흡음재").ne("○"), | ||
| Criteria.where("description").not().regex("흡음재") | ||
| )); | ||
| } | ||
| } |
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.
문자열 비교에 '==' 사용: equals로 교체 필요
Java에서 문자열 비교는 ==가 아니라 equals를 사용해야 합니다. 현재 분기는 항상 false가 될 수 있습니다.
적용 diff:
- if (soundDampener != null) {
- if (soundDampener == KeyboardOptions.SoundDampener.YES.getValue()) {
+ if (soundDampener != null) {
+ if (KeyboardOptions.SoundDampener.YES.getValue().equals(soundDampener)) {
andCriterias.add(new Criteria().orOperator(
Criteria.where("spec_table.키보드구조 > 흡음재").is("○"),
Criteria.where("description").regex("흡음재")
));
} else {
query.addCriteria(new Criteria().andOperator(
Criteria.where("spec_table.키보드구조 > 흡음재").ne("○"),
Criteria.where("description").not().regex("흡음재")
));
}
}📝 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.
| // SoundDampener | |
| if (soundDampener != null) { | |
| if (soundDampener == KeyboardOptions.SoundDampener.YES.getValue()) { | |
| andCriterias.add(new Criteria().orOperator( | |
| Criteria.where("spec_table.키보드구조 > 흡음재").is("○"), | |
| Criteria.where("description").regex("흡음재") | |
| )); | |
| } else { | |
| query.addCriteria(new Criteria().andOperator( | |
| Criteria.where("spec_table.키보드구조 > 흡음재").ne("○"), | |
| Criteria.where("description").not().regex("흡음재") | |
| )); | |
| } | |
| } | |
| // SoundDampener | |
| if (soundDampener != null) { | |
| if (KeyboardOptions.SoundDampener.YES.getValue().equals(soundDampener)) { | |
| andCriterias.add(new Criteria().orOperator( | |
| Criteria.where("spec_table.키보드구조 > 흡음재").is("○"), | |
| Criteria.where("description").regex("흡음재") | |
| )); | |
| } else { | |
| query.addCriteria(new Criteria().andOperator( | |
| Criteria.where("spec_table.키보드구조 > 흡음재").ne("○"), | |
| Criteria.where("description").not().regex("흡음재") | |
| )); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java
around lines 87-100, the code uses '==' to compare strings for soundDampener
which is incorrect; replace the identity comparison with a proper equals check
(e.g. use Objects.equals(soundDampener,
KeyboardOptions.SoundDampener.YES.getValue()) or call
KeyboardOptions.SoundDampener.YES.getValue().equals(soundDampener) to avoid NPE)
while keeping the existing null guard, so the branch correctly detects the "YES"
value and the other branch runs for non-YES values.
| if (rgb != null) { | ||
| if (rgb == KeyboardOptions.RGB.YES.getValue()) { | ||
| andCriterias.add(new Criteria().orOperator( | ||
| Criteria.where("spec_table.키보드구조 > RGB 백라이트").is("○"), | ||
| Criteria.where("description").regex("RGB", "i") | ||
| )); | ||
| } else { | ||
| query.addCriteria(new Criteria().andOperator( | ||
| Criteria.where("spec_table.키보드구조 > RGB 백라이트").ne("○"), | ||
| Criteria.where("description").not().regex("RGB", "i") | ||
| )); | ||
| } | ||
| } |
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.
문자열 비교에 '==' 사용(2): RGB도 동일 이슈
위와 같은 이유로 RGB도 equals로 교체하세요.
적용 diff:
- if (rgb != null) {
- if (rgb == KeyboardOptions.RGB.YES.getValue()) {
+ if (rgb != null) {
+ if (KeyboardOptions.RGB.YES.getValue().equals(rgb)) {
andCriterias.add(new Criteria().orOperator(
Criteria.where("spec_table.키보드구조 > RGB 백라이트").is("○"),
Criteria.where("description").regex("RGB", "i")
));
} else {
query.addCriteria(new Criteria().andOperator(
Criteria.where("spec_table.키보드구조 > RGB 백라이트").ne("○"),
Criteria.where("description").not().regex("RGB", "i")
));
}
}📝 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.
| if (rgb != null) { | |
| if (rgb == KeyboardOptions.RGB.YES.getValue()) { | |
| andCriterias.add(new Criteria().orOperator( | |
| Criteria.where("spec_table.키보드구조 > RGB 백라이트").is("○"), | |
| Criteria.where("description").regex("RGB", "i") | |
| )); | |
| } else { | |
| query.addCriteria(new Criteria().andOperator( | |
| Criteria.where("spec_table.키보드구조 > RGB 백라이트").ne("○"), | |
| Criteria.where("description").not().regex("RGB", "i") | |
| )); | |
| } | |
| } | |
| if (rgb != null) { | |
| if (KeyboardOptions.RGB.YES.getValue().equals(rgb)) { | |
| andCriterias.add(new Criteria().orOperator( | |
| Criteria.where("spec_table.키보드구조 > RGB 백라이트").is("○"), | |
| Criteria.where("description").regex("RGB", "i") | |
| )); | |
| } else { | |
| query.addCriteria(new Criteria().andOperator( | |
| Criteria.where("spec_table.키보드구조 > RGB 백라이트").ne("○"), | |
| Criteria.where("description").not().regex("RGB", "i") | |
| )); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java
around lines 104 to 116, the code compares the rgb string using '==' which is
incorrect for string equality; replace the identity comparison with a safe
equals check (prefer using KeyboardOptions.RGB.YES.getValue().equals(rgb) to
avoid NPE) so the if-condition becomes a proper string equality test; keep the
existing null guard and leave the inner Criteria logic unchanged.
| // 가격 | ||
| if (minPrice > 0 || maxPrice > 0) { | ||
| query.addCriteria(Criteria.where("options").elemMatch( | ||
| Criteria.where("main_price").gte(minPrice).lte(maxPrice) | ||
| )); | ||
| } |
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.
가격 필터 경계 처리 버그: min/max 단독 제공 시 결과 0건 가능
현재는 둘 중 하나만 제공돼도 gte(min).lte(max)가 동시에 들어가서, 예를 들어 minPrice>0, maxPrice=0이면 모든 결과가 필터아웃됩니다. 각 경계는 개별적으로 적용하세요.
적용 diff:
- if (minPrice > 0 || maxPrice > 0) {
- query.addCriteria(Criteria.where("options").elemMatch(
- Criteria.where("main_price").gte(minPrice).lte(maxPrice)
- ));
- }
+ if (minPrice > 0 || maxPrice > 0) {
+ List<Criteria> priceBounds = new ArrayList<>();
+ if (minPrice > 0) priceBounds.add(Criteria.where("main_price").gte(minPrice));
+ if (maxPrice > 0) priceBounds.add(Criteria.where("main_price").lte(maxPrice));
+ query.addCriteria(Criteria.where("options")
+ .elemMatch(new Criteria().andOperator(priceBounds.toArray(new Criteria[0]))));
+ }📝 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.
| // 가격 | |
| if (minPrice > 0 || maxPrice > 0) { | |
| query.addCriteria(Criteria.where("options").elemMatch( | |
| Criteria.where("main_price").gte(minPrice).lte(maxPrice) | |
| )); | |
| } | |
| // 가격 | |
| if (minPrice > 0 || maxPrice > 0) { | |
| List<Criteria> priceBounds = new ArrayList<>(); | |
| if (minPrice > 0) priceBounds.add(Criteria.where("main_price").gte(minPrice)); | |
| if (maxPrice > 0) priceBounds.add(Criteria.where("main_price").lte(maxPrice)); | |
| query.addCriteria(Criteria.where("options") | |
| .elemMatch(new Criteria().andOperator(priceBounds.toArray(new Criteria[0])))); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/out/RecommendKeyboardAdapter.java
around lines 118 to 123, the price filter always applies both gte(minPrice) and
lte(maxPrice) even when one boundary is absent which can filter out all results
(e.g., minPrice>0 and maxPrice==0); fix by building the Criteria for
"options".elemMatch dynamically: if minPrice>0 add a gte condition, if
maxPrice>0 add a lte condition, and only chain both when both are present;
replace the current hardcoded
Criteria.where("main_price").gte(minPrice).lte(maxPrice) with a Criteria built
conditionally and passed to elemMatch so single-sided filters work correctly.
| List<Product> documents=recommendKeyboardPort.filterAndRecommendKeyboards( | ||
| userId, | ||
| KeyboardOptionsConverter.mapSizeToDescription(request.getSize()), | ||
| KeyboardOptionsConverter.mapKeyPressureToSpecTable(request.getKeyPressure()), | ||
| KeyboardOptionsConverter.mapLayoutToDescription(request.getLayout()), | ||
| KeyboardOptionsConverter.mapSwitchTypeToOptionNames(request.getSwitchType()), | ||
| request.getSoundDampener().getValue(), | ||
| request.getRgb().getValue(), | ||
| request.getMinPrice(), | ||
| request.getMaxPrice() | ||
| ); |
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.
NPE 위험: null 언박싱 및 필드 null 접근 가능성
request.getSoundDampener().getValue()/request.getRgb().getValue()는 각 필드가 null이면 NPE.getMinPrice()/getMaxPrice()가Integer라면, 포트 시그니처가int일 때 오토 언박싱 NPE.mapSwitchTypeToOptionNames가 빈 리스트를 반환하면$in []로 0건 매칭 위험.
안전한 디폴트/널 처리 후 포트로 전달해 주세요.
- List<Product> documents=recommendKeyboardPort.filterAndRecommendKeyboards(
- userId,
- KeyboardOptionsConverter.mapSizeToDescription(request.getSize()),
- KeyboardOptionsConverter.mapKeyPressureToSpecTable(request.getKeyPressure()),
- KeyboardOptionsConverter.mapLayoutToDescription(request.getLayout()),
- KeyboardOptionsConverter.mapSwitchTypeToOptionNames(request.getSwitchType()),
- request.getSoundDampener().getValue(),
- request.getRgb().getValue(),
- request.getMinPrice(),
- request.getMaxPrice()
- );
+ // Null-safe 변환
+ String sizeDesc = KeyboardOptionsConverter.mapSizeToDescription(request.getSize());
+ Integer keyPressure = KeyboardOptionsConverter.mapKeyPressureToSpecTable(request.getKeyPressure());
+ String layoutDesc = KeyboardOptionsConverter.mapLayoutToDescription(request.getLayout());
+
+ List<String> switchNames = KeyboardOptionsConverter.mapSwitchTypeToOptionNames(request.getSwitchType());
+ List<String> switchFilter = (switchNames == null || switchNames.isEmpty()) ? null : switchNames;
+
+ String soundDampener = (request.getSoundDampener() != null) ? request.getSoundDampener().getValue() : null;
+ String rgb = (request.getRgb() != null) ? request.getRgb().getValue() : null;
+
+ int minPrice = (request.getMinPrice() != null) ? request.getMinPrice() : 0;
+ int maxPrice = (request.getMaxPrice() != null) ? request.getMaxPrice() : Integer.MAX_VALUE;
+
+ List<Product> documents = recommendKeyboardPort.filterAndRecommendKeyboards(
+ userId,
+ sizeDesc,
+ keyPressure,
+ layoutDesc,
+ switchFilter,
+ soundDampener,
+ rgb,
+ minPrice,
+ maxPrice
+ );📝 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.
| List<Product> documents=recommendKeyboardPort.filterAndRecommendKeyboards( | |
| userId, | |
| KeyboardOptionsConverter.mapSizeToDescription(request.getSize()), | |
| KeyboardOptionsConverter.mapKeyPressureToSpecTable(request.getKeyPressure()), | |
| KeyboardOptionsConverter.mapLayoutToDescription(request.getLayout()), | |
| KeyboardOptionsConverter.mapSwitchTypeToOptionNames(request.getSwitchType()), | |
| request.getSoundDampener().getValue(), | |
| request.getRgb().getValue(), | |
| request.getMinPrice(), | |
| request.getMaxPrice() | |
| ); | |
| // Null-safe 변환 | |
| String sizeDesc = KeyboardOptionsConverter.mapSizeToDescription(request.getSize()); | |
| Integer keyPressure = KeyboardOptionsConverter.mapKeyPressureToSpecTable(request.getKeyPressure()); | |
| String layoutDesc = KeyboardOptionsConverter.mapLayoutToDescription(request.getLayout()); | |
| List<String> switchNames = KeyboardOptionsConverter.mapSwitchTypeToOptionNames(request.getSwitchType()); | |
| List<String> switchFilter = (switchNames == null || switchNames.isEmpty()) ? null : switchNames; | |
| String soundDampener = (request.getSoundDampener() != null) | |
| ? request.getSoundDampener().getValue() | |
| : null; | |
| String rgb = (request.getRgb() != null) | |
| ? request.getRgb().getValue() | |
| : null; | |
| int minPrice = (request.getMinPrice() != null) | |
| ? request.getMinPrice() | |
| : 0; | |
| int maxPrice = (request.getMaxPrice() != null) | |
| ? request.getMaxPrice() | |
| : Integer.MAX_VALUE; | |
| List<Product> documents = recommendKeyboardPort.filterAndRecommendKeyboards( | |
| userId, | |
| sizeDesc, | |
| keyPressure, | |
| layoutDesc, | |
| switchFilter, | |
| soundDampener, | |
| rgb, | |
| minPrice, | |
| maxPrice | |
| ); |
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/application/service/RecommendationService.java
around lines 108-118, the call chain can throw NPEs and produce an empty-filter
match: guard against nulls before passing to recommendKeyboardPort — check
request.getSoundDampener() and request.getRgb() and use safe defaults (e.g.,
false or an explicit default value) instead of calling getValue() on a possibly
null object; treat request.getMinPrice()/getMaxPrice() (Integer) by providing
primitive defaults when null (e.g., 0 and Integer.MAX_VALUE or your domain
default) to avoid auto-unboxing NPE; when mapSwitchTypeToOptionNames(...)
returns an empty list, convert it to null or omit it so the port does not
produce an $in [] filter that matches nothing; finally pass these sanitized
values to filterAndRecommendKeyboards.
- 같은 제조사 상품으로 추가 - 만약 그래도 없으면 카테고리 내 랜덤하게 채우도록 (6개)
…KiKi-Hi/Platform-Server into feat/custom/KIKI-73-BE-추천-기능-구현
doup2001
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.
전체적으로 코드 잘 보았습니다!!! 궁금한 점 코멘트 남겨두었습니당!!
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public class KeyboardOptionsConverter { |
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.
축의 정도를 한번에 다 converter로 처리하게끔 했군요!!
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| @Schema(name = "[요청][상품] 키압(Key Pressure) Enum", description = "키압 옵션") |
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.
요기 @Schema 내용 수정해야 될 것 같아욤!
| .manufacturerName(product.getManufacturer()) | ||
| .productName(product.getName()) | ||
| .price(product.getPrice()) | ||
| .likedByMe(false) |
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.
항상 false이면 안되지않을까요?! 상품과 유저 기반 boolean을 받는 from 메서드도 필요할 것 같습니당!
| private List<String> allDetailImages; // | ||
|
|
||
| @Field("spec_table") | ||
| private Map<String, Object> specTable; // spec_table은 현재 비어있는 상태로 초기화 |
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.
specTable에서 꺼내 와서 비교하기에 컬럼을 추가한거군요! specTable에서 다 꺼내와야되기에 아주 먼 훗날 데이터의 정규화가 필요하긴 하겠네욤...!
| * @return 추천 상품 리스트 | ||
| */ | ||
| @Override | ||
| public List<KeyboardRecommendationResponse> getTutorialKeyboardRecommendation(UUID userId, KeyboardRecommendationRequest request) { |
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.
키보드 추천을 위한 필터링 로직 잘 봤습니당!! 👀👍
필터링을 하고나서 UserId를 바탕으로 저장하는 로직도 필요할 것 같아요!
| * @return | ||
| */ | ||
| @Override | ||
| public List<Product> getSimilarProducts(UUID userId, String productId) { |
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.
비슷한 상품을 추천할 때 UserId가 필요한 이유는 무엇일까요??👀
📌 작업한 내용
🔍 참고 사항
🖼️ 스크린샷
🔗 관련 이슈
#67
✅ 체크리스트
@coderabbitai review
Summary by CodeRabbit