Skip to content
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.jiyoung.kikihi'
group = 'site.kikihi.custom'
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

그룹 ID 변경 영향도 점검 및 잔존 패키지 네임스페이스 탐지 제안

artifact 좌표가 com.jiyoung.kikihisite.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.gradle

Length of output: 1494


그룹 ID 변경 후 잔존 패키지 네임스페이스 수동 수정 필요

  • src/main/java/site/kikihi/custom/global/logging/ControllerLoggingAspect.java (24행):
    @pointcut에 남아있는 com.jiyoung.kikihi.platform.adapter.in.website.kikihi.custom.platform.adapter.in.web으로 수정
  • src/main/java/site/kikihi/custom/global/logging/LoggingAspect.java (16행):
    @pointcut에 남아있는 com.jiyoung.kikihi.platform.application.servicesite.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.

version = '0.0.1-SNAPSHOT'

java {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package site.kikihi.custom.platform.adapter.in.web;

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import site.kikihi.custom.global.response.ApiResponse;
import site.kikihi.custom.platform.adapter.in.web.dto.request.product.KeyboardRecommendationRequest;
import site.kikihi.custom.platform.adapter.in.web.dto.response.product.KeyboardRecommendationResponse;
import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse;
import site.kikihi.custom.platform.adapter.in.web.swagger.RecommendControllerSpec;
import site.kikihi.custom.platform.application.in.recommendation.RecommendationUseCase;
import site.kikihi.custom.platform.domain.product.Product;
import site.kikihi.custom.security.oauth2.domain.PrincipalDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -43,6 +44,41 @@ public ApiResponse<List<ProductListResponse>> getProductRecommendation(
return ApiResponse.ok(ProductListResponse.from(recommendation));
}

/**
* 튜토리얼 키보드 추천 API
*/
@PostMapping("/tutorial")
public ApiResponse<List<KeyboardRecommendationResponse>> getTutorialKeyboardRecommendation(
@AuthenticationPrincipal PrincipalDetails principalDetails,
@Valid @RequestBody KeyboardRecommendationRequest request
) {

// 유저가 존재하면 넣기
UUID userId = principalDetails != null ? principalDetails.getId() : null;

// 튜토리얼 키보드 추천 서비스 호출
List<KeyboardRecommendationResponse> recommendation = service.getTutorialKeyboardRecommendation(userId,request);

// 응답 주기
return ApiResponse.ok(recommendation);
}


/**
* 유사한 상품 추천
*/
@GetMapping("/{productId}")
public ApiResponse<List<KeyboardRecommendationResponse>> getSimilarProducts(
@PathVariable("productId") String productId,
@AuthenticationPrincipal PrincipalDetails principalDetails
) {
// 유저가 존재하면 넣기
UUID userId = principalDetails != null ? principalDetails.getId() : null;

// 유사한 상품 추천 서비스 호출
List<Product> similarProducts = service.getSimilarProducts(userId,productId);

// 응답 주기
return ApiResponse.ok(KeyboardRecommendationResponse.from(similarProducts));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package site.kikihi.custom.platform.adapter.in.web.converter;

import site.kikihi.custom.platform.adapter.in.web.dto.request.product.KeyboardOptions;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class KeyboardOptionsConverter {
Copy link
Member

Choose a reason for hiding this comment

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

축의 정도를 한번에 다 converter로 처리하게끔 했군요!!


// Size enum → description 검색 키워드 맵핑
public static String mapSizeToDescription(KeyboardOptions.Size size) {
if (size == null) return null;
return switch (size) {
case TENKEYLESS -> "텐키리스";
case FULL -> "풀배열";
case MINI -> "미니";
};
}

// SwitchType enum → options.option_name 중 매칭되는 문자열 배열 리턴
public static List<String> mapSwitchTypeToOptionNames(KeyboardOptions.SwitchType switchType) {
if (switchType == null) return Collections.emptyList();

return switch (switchType) {
case SILENT -> Arrays.asList(
"저소음 적축", "저소음 갈축", "저소음 흑축",
"저소음 바다축", "저소음 잉크축", "저소음 바닐라축",
"저소음 딸기축", "저소음 바나나축"
);
case NORMAL -> Arrays.asList(
"갈축", "바나나축", "바닐라축",
"핑크축", "레몬축", "딸기축", "경해축",
"잉크축 V2", "모가축", "판다축",
"바다축", "라벤더축", "체리 스피드 실버",
"리니어 옵티컬"
);
case LOUD -> Arrays.asList(
"청축", "녹축","백축","clicky"
);
case SMOOTH -> Arrays.asList(
"적축", "흑축", "자석축", "광축",
"실버축", "스피드 적축", "잉크축",
"바다축", "바닐라축", "체리 리니어 옵티컬",
"라떼축", "모카축", "사파이어축","밀키축",
"무지개축", "크림축"
);
};

}

// Layout enum → description 검색 키워드 맵핑
public static String mapLayoutToDescription(KeyboardOptions.Layout layout) {
if (layout == null) return null;
return switch (layout) {
case ERGONOMIC -> "스텝스컬쳐2";
case SIMPLE -> "로우프로파일(LP)";
};
}

// 키압에서 g빼기
public static Integer mapKeyPressureToSpecTable(KeyboardOptions.KeyPressure keyPressure) {
if (keyPressure == null) return null;
return switch (keyPressure) {
case LIGHT -> 49;
case NORMAL -> 50;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package site.kikihi.custom.platform.adapter.in.web.dto.request.product;

import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
@Schema(
name = "[요청][상품] 키보드 옵션 Enum",
description = "키보드 추천 서비스에서 사용하는 키보드 옵션 Enum입니다."
)
public class KeyboardOptions {

@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;
}
}
Comment on lines +16 to +35
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.


@Getter
@RequiredArgsConstructor
@Schema(name = "[요청][상품] 키압(Key Pressure) Enum", description = "키압 옵션")
public enum KeyPressure {
@Schema(description = "가벼운 키압 (50g 미만)", example = "light")
LIGHT("light"),

@Schema(description = "보통 키압 (50g 이상)", example = "normal")
NORMAL("normal");

private final String value;

@JsonValue
public String getValue() {
return value;
}
}

@Getter
@RequiredArgsConstructor
@Schema(name = "[요청][상품] 키압(Key Pressure) Enum", description = "키압 옵션")
Copy link
Member

Choose a reason for hiding this comment

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

요기 @Schema 내용 수정해야 될 것 같아욤!

public enum Layout {
@Schema(description = "인체공학적 (스텝스컬쳐2)", example = "egonomic")
ERGONOMIC("egonomic"),

@Schema(description = "심플하고 깔끔한 (low 프로파일)", example = "simple")
SIMPLE("simple");
Comment on lines +57 to +63
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

스키마 설명 라벨 오류

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.

Suggested change
@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.


private final String value;

@JsonValue
public String getValue() {
return value;
}
}

@Getter
@RequiredArgsConstructor
@Schema(name = "[요청][상품] 스위치 종류(Switch Type) Enum", description = "스위치 종류 옵션")
public enum SwitchType {

@Schema(description = "조용한", example = "silent")
SILENT("silent"),

@Schema(description = "적당한", example = "normal")
NORMAL("normal"),

@Schema(description = "강한", example = "loud")
LOUD("loud"),

@Schema(description = "부드러운", example = "smooth")
SMOOTH("smooth");

private final String value;

@JsonValue
public String getValue() {
return value;
}
}

@Getter
@RequiredArgsConstructor
@Schema(name = "[요청][상품] 흡음재 여부 Enum", description = "흡음재 유무 옵션")
public enum SoundDampener {
@Schema(description = "흡음재 있음", example = "○")
YES("○"),

@Schema(description = "흡음재 없음", example = "X")
NO("X");

private final String value;

@JsonValue
public String getValue() {
return value;
}
}

@Getter
@RequiredArgsConstructor
@Schema(name = "[요청][상품] RGB 여부 Enum", description = "RGB 유무 옵션")
public enum RGB {
@Schema(description = "RGB 있음", example = "○")
YES("○"),

@Schema(description = "RGB 없음", example = "X")
NO("X");

private final String value;

@JsonValue
public String getValue() {
return value;
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package site.kikihi.custom.platform.adapter.in.web.dto.request.product;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Schema(name = "[요청][상품] 키보드 추천 요청 DTO", description = "키보드 추천 요청에 사용하는 DTO입니다.")
public class KeyboardRecommendationRequest {

@Schema(description = "키보드 배열(Size) 옵션", example = "ten", required = true)
private KeyboardOptions.Size size;

@Schema(description = "키압(Key Pressure) 옵션", example = "light", required = true)
private KeyboardOptions.KeyPressure keyPressure;

@Schema(description = "레이아웃 종류(Layout) 옵션", example = "egonomic", required = true)
private KeyboardOptions.Layout layout;

@Schema(description = "스위치 종류(Switch Type) 옵션", example = "silent", required = true)
private KeyboardOptions.SwitchType switchType;

@Schema(description = "흡음재(Sound Dampener) 적용 여부", example = "○", required = true)
private KeyboardOptions.SoundDampener soundDampener;

@Schema(description = "RGB 적용 여부", example = "○", required = true)
private KeyboardOptions.RGB rgb;

@Schema(description = "최소 가격 (단위: 원)", example = "0", required = true)
private int minPrice;

@Schema(description = "최대 가격 (단위: 원)", example = "200000", required = true)
private int maxPrice;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package site.kikihi.custom.platform.adapter.in.web.dto.response.product;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import site.kikihi.custom.platform.domain.product.Product;

import java.util.List;

/**
* 상품 상세 응답 DTO
*
* @param id 상품 ID
* @param thumbnail 상품 썸네일
* @param manufacturerName 제조사명
* @param productName 제품명
* @param price
* @param likedByMe 나의 북마크 여부
*/

@Builder
@Schema(name = "KeyboardRecommendationListResponse", description = "튜토리얼 키보드 추천 리스트 응답")
public record KeyboardRecommendationResponse(

@Schema(description = "상품 아이디", example = "101")
String id,

@Schema(description = "상품 썸네일 이미지 URL", example = "https://example.com/product/101.jpg")
String thumbnail,

@Schema(description = "제조사명", example = "독거미")
String manufacturerName,

@Schema(description = "제품명", example = "독거미 Aula F99")
String productName,

@Schema(description = "정상가(원)", example = "599000.0")
double price,
Comment on lines +36 to +37
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.


@Schema(description = "북마크(좋아요)한 상품 여부", example = "true")
boolean likedByMe

) {

/// 정적 팩토리 메서드
// 단일 객체 변환 메서드 추가
public static KeyboardRecommendationResponse from(Product product) {
return KeyboardRecommendationResponse.builder()
.id(product.getId())
.thumbnail(product.getThumbnail())
.manufacturerName(product.getManufacturer())
.productName(product.getName())
.price(product.getPrice())
.likedByMe(false)
Copy link
Member

Choose a reason for hiding this comment

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

항상 false이면 안되지않을까요?! 상품과 유저 기반 boolean을 받는 from 메서드도 필요할 것 같습니당!

.build();
}


// 기존 리스트 변환 메서드는 그대로 유지
public static List<KeyboardRecommendationResponse> from(List<Product> productList) {
return productList.stream()
.map(KeyboardRecommendationResponse::from) // 단일 객체 변환 메서드 호출
.toList();
}



}

Loading
Loading