diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/CustomKeyboardController.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/CustomKeyboardController.java index 3efaf06..10b7e3b 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/CustomKeyboardController.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/CustomKeyboardController.java @@ -135,6 +135,9 @@ public ApiResponse> getCustomKeyboardProducts /// 서비스 Slice content = service.getCustomProducts(userId, category.getValue(), layout, pageable); + /// 개수 조회 + Long counts = service.getCustomProductCounts(category.getValue(), layout); + /// DTO 변경 Slice dtoSlice = new SliceImpl<>( content.getContent(), @@ -143,7 +146,7 @@ public ApiResponse> getCustomKeyboardProducts ); /// 응답 - return ApiResponse.ok(SliceResponse.from(dtoSlice)); + return ApiResponse.ok(SliceResponse.from(dtoSlice, counts)); } /** diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomKeyboardRequest.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomKeyboardRequest.java index c3c4c36..06bf921 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomKeyboardRequest.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomKeyboardRequest.java @@ -26,6 +26,9 @@ public class CustomKeyboardRequest { @Schema(description = "키캡 ID", example = "keycap_29") private String keyCapId; + @Schema(description = "악세사리 ID", example = "accessory_id") + private String accessoryId; + @Schema(description = "커스텀 키보드 이름", example = "내 인생 첫 커스텀 65키") private String name; diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/custom/CustomKeyboardDetailResponse.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/custom/CustomKeyboardDetailResponse.java index 34ede59..2ec157f 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/custom/CustomKeyboardDetailResponse.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/custom/CustomKeyboardDetailResponse.java @@ -21,24 +21,39 @@ description = "커스텀 키보드의 상세 정보를 조회하는 응답 DTO입니다." ) public record CustomKeyboardDetailResponse( - @Schema(description = "커스텀 키보드 ID", example = "101") + @Schema(description = "커스텀 키보드 ID", example = "1") Long customId, @Schema(description = "커스텀 키보드 이름", example = "타건감 좋은 65키 키보드") String customName, - @Schema(description = "커스텀 키보드 타입", example = "65%") + @Schema(description = "커스텀 키보드 타입", example = "60배열") String customKeyboardType, + @Schema(description = "하우징 상품 ID", example = "68b3f4fedc26d32d8881fe12") + String housingId, + @Schema(description = "하우징(프레임) 이름", example = "TX-65") String housingName, + @Schema(description = "키캡 ID", example = "GMK Red Samurai") + String keyCapId, + @Schema(description = "키캡 이름", example = "GMK Red Samurai") String keyCapName, - @Schema(description = "스위치 이름", example = "Gateron Ink Black v2") + @Schema(description = "스위치 ID", example = "689d4fd15f80b10f1691fc96") + String switchId, + + @Schema(description = "스위치 이름", example = "게이트론 브라운 스위치 (10개) - 갈축 10개") String switchName, + @Schema(description = "악세사리 ID", example = "68baf9e37de370ef29647eb1") + String accessoryId, + + @Schema(description = "악세사리 이름", example = "아크릴 키보드 덮개 케이스 커버 보관함 방진 중형 보호 먼지 차단 투명 수납함 정리함") + String accessoryName, + @Schema(description = "썸네일 이미지 URL", example = "https://example.com/custom/101.jpg") String thumbnail, @@ -52,9 +67,14 @@ public static CustomKeyboardDetailResponse from(CustomKeyboardWithName entity){ .customId(entity.id()) .customName(entity.name()) .customKeyboardType(entity.layout()) + .housingId(entity.frameId()) .housingName(entity.frameName()) + .keyCapId(entity.keyCapId()) .keyCapName(entity.keyCapName()) + .switchId(entity.switchId()) .switchName(entity.switchName()) + .accessoryId(entity.accessoryId()) + .accessoryName(entity.accessoryName()) .thumbnail(entity.imageUrl()) .price(entity.totalPrice()) .build(); diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductListResponse.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductListResponse.java index 549ddf0..5bd1575 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductListResponse.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductListResponse.java @@ -1,6 +1,7 @@ package site.kikihi.custom.platform.adapter.in.web.dto.response.product; +import com.fasterxml.jackson.annotation.JsonInclude; import site.kikihi.custom.platform.domain.product.Product; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @@ -21,6 +22,7 @@ * @param discountedPrice 가격 (기존 DTO 컬럼과 동일하게 유지) */ +@JsonInclude(JsonInclude.Include.NON_NULL) @Builder @Schema(name = "[응답][상품] 상품 목록 조회 Response", description = "상품 목록 조회를 위한 DTO입니다.") public record ProductListResponse( @@ -31,6 +33,9 @@ public record ProductListResponse( @Schema(description = "상품 썸네일 이미지 URL", example = "https://example.com/product/101.jpg") String thumbnail, + @Schema(description = "상품 매핑 이미지 URL", example = "https://example.com/product/101.jpg") + String mappingUrl, + @Schema(description = "카테고리명", example = "keycap") String category, @@ -53,6 +58,7 @@ public static ProductListResponse from(Product product) { .id(product.getId()) .thumbnail(product.getThumbnail()) .category(product.getCategory()) + .mappingUrl(product.getMapping()) .manufacturerName(product.getManufacturer()) .productName(product.getName()) .discountedPrice(getPrice(product)) @@ -81,6 +87,7 @@ public static ProductListResponse from(Product product, boolean likedByMe) { return ProductListResponse.builder() .id(product.getId()) .thumbnail(product.getThumbnail()) + .mappingUrl(product.getMapping()) .category(product.getCategory()) .manufacturerName(product.getManufacturer()) .productName(product.getName()) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/CustomKeyboardControllerSpec.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/CustomKeyboardControllerSpec.java index fd94283..ecbe015 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/CustomKeyboardControllerSpec.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/CustomKeyboardControllerSpec.java @@ -1,5 +1,6 @@ package site.kikihi.custom.platform.adapter.in.web.swagger; +import io.swagger.v3.oas.annotations.Parameter; import site.kikihi.custom.global.response.ApiResponse; import site.kikihi.custom.global.response.page.PageRequest; import site.kikihi.custom.global.response.page.SliceResponse; @@ -52,13 +53,15 @@ ApiResponse createCustomKeyboard( /** * 키보드 상세 조회 - * @param customKeyboardId 키보드 ID + * + * @param customKeyboardId 키보드 ID */ @Operation( summary = "커스텀 키보드 상세 조회 API", description = "커스텀 키보드를 상세 조회합니다." ) ApiResponse getCustomKeyboard( + @Parameter(example = "1") @PathVariable Long customKeyboardId); @@ -112,9 +115,10 @@ ApiResponse deleteCustomKeyboard( String REQUEST = """ { "layout" : "PERCENT_60", - "housingId" : "68b3f4fedc26d32d8881fdff", - "switchId" : "686bd26d34c3c12ea9b8e7e3", - "keyCapId" : "68b3f653dc26d32d8881fe3d", + "housingId" : "68b3f4fedc26d32d8881fe12", + "switchId" : "689d4fd15f80b10f1691fc96", + "keyCapId" : "68b3f653dc26d32d8881fe43", + "accessoryId" : "68baf9e37de370ef29647eb1", "name" : "테스트 커스텀" } """; diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/ProductMongoAdapter.java b/src/main/java/site/kikihi/custom/platform/adapter/out/ProductMongoAdapter.java index 1b48ee6..fe40e07 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/ProductMongoAdapter.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/ProductMongoAdapter.java @@ -140,12 +140,22 @@ public Slice getProductsAndCategoryByType(String type, String categoryI .map(ProductDocument::toDomain); } + @Override + public Long getProductsAndCategoryByType(String type, String categoryId) { + return documentRepository.countByTypeAndCategoryAndIsCustomTrue(type, categoryId); + } + @Override public Slice getProductsByCategoryAndCustom(String categoryId, Pageable pageable) { return documentRepository.findByCategoryAndIsCustomTrue(categoryId, pageable) .map(ProductDocument::toDomain); } + @Override + public Long getProductsByCategoryAndCustom(String categoryId) { + return documentRepository.countByCategoryAndIsCustomTrue(categoryId); + } + /** * 상품 개수 조회 diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/custom/CustomKeyboardJpaEntity.java b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/custom/CustomKeyboardJpaEntity.java index ae603ef..dfe468f 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/custom/CustomKeyboardJpaEntity.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/jpa/custom/CustomKeyboardJpaEntity.java @@ -30,6 +30,8 @@ public class CustomKeyboardJpaEntity extends BaseTimeEntity { private String keyCapId; + private String accessoryId; + private String name; private String imageUrl; @@ -45,6 +47,7 @@ public static CustomKeyboardJpaEntity from(CustomKeyboard entity) { .frameId(entity.getFrameId()) .switchId(entity.getSwitchId()) .keyCapId(entity.getKeyCapId()) + .accessoryId(entity.getAccessoryId()) .name(entity.getName()) .imageUrl(entity.getImageUrl()) .layout(entity.getLayout()) @@ -59,6 +62,7 @@ public CustomKeyboard toDomain(){ .frameId(frameId) .switchId(switchId) .keyCapId(keyCapId) + .accessoryId(accessoryId) .name(name) .imageUrl(imageUrl) .layout(layout) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java b/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java index 3e7c869..3f5e0bf 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java @@ -35,6 +35,9 @@ public class ProductDocument { @Field("thumbnail") private String thumbnail; + @Field("mapping") + private String mapping; + private String manufacturer; @Field("detail_page_url") @@ -65,6 +68,7 @@ public Product toDomain(){ .price(price) .description(description) .thumbnail(thumbnail) + .mapping(mapping) .manufacturer(manufacturer) .detailPageUrl(detailPageUrl) .options(options) diff --git a/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocumentRepository.java b/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocumentRepository.java index 9ac68dd..8b51a46 100644 --- a/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocumentRepository.java +++ b/src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocumentRepository.java @@ -79,6 +79,11 @@ Slice findByTypeAndCategoryAndIsCustomTrue( Pageable pageable ); + @Query(value = """ + { 'type': ?0, 'category': ?1, 'is_custom': true } +""", count = true) + Long countByTypeAndCategoryAndIsCustomTrue(String type, String category); + @Query(""" { 'category': ?0, @@ -90,6 +95,14 @@ Slice findByCategoryAndIsCustomTrue( Pageable pageable ); + + @Query(value = """ + {'category': ?0, 'is_custom': true } +""", count = true) + Long countByCategoryAndIsCustomTrue(String category); + + + /// 아이디 기반 조회 Slice findByIdIn(List ids, Pageable pageable); diff --git a/src/main/java/site/kikihi/custom/platform/application/in/custom/CustomKeyboardUseCase.java b/src/main/java/site/kikihi/custom/platform/application/in/custom/CustomKeyboardUseCase.java index 6f0e135..fe17586 100644 --- a/src/main/java/site/kikihi/custom/platform/application/in/custom/CustomKeyboardUseCase.java +++ b/src/main/java/site/kikihi/custom/platform/application/in/custom/CustomKeyboardUseCase.java @@ -40,6 +40,9 @@ public interface CustomKeyboardUseCase { // 배열에 맞는 상품 조회하기 Slice getCustomProducts(UUID userId, String categoryId, CustomKeyboardLayout type, Pageable pageable); + // 배여에 맞는 상품 개수 조회하기 + Long getCustomProductCounts(String categoryId, CustomKeyboardLayout type); + /// 삭제 void deleteCustomKeyboard(Long customKeyboardId, UUID userId); diff --git a/src/main/java/site/kikihi/custom/platform/application/out/product/ProductPort.java b/src/main/java/site/kikihi/custom/platform/application/out/product/ProductPort.java index e88403a..8c4739c 100644 --- a/src/main/java/site/kikihi/custom/platform/application/out/product/ProductPort.java +++ b/src/main/java/site/kikihi/custom/platform/application/out/product/ProductPort.java @@ -50,8 +50,14 @@ public interface ProductPort { Slice getProductsAndCategoryByType(String type, String categoryId, Pageable pageable); + // 개수 + Long getProductsAndCategoryByType(String type, String categoryId); + Slice getProductsByCategoryAndCustom(String categoryId, Pageable pageable); + // 개수 + Long getProductsByCategoryAndCustom(String categoryId); + /// 상품 개수 조회 Long getProductsCount(String category); diff --git a/src/main/java/site/kikihi/custom/platform/application/service/CustomKeyboardService.java b/src/main/java/site/kikihi/custom/platform/application/service/CustomKeyboardService.java index e6171fc..49c55f0 100644 --- a/src/main/java/site/kikihi/custom/platform/application/service/CustomKeyboardService.java +++ b/src/main/java/site/kikihi/custom/platform/application/service/CustomKeyboardService.java @@ -25,6 +25,7 @@ import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * 키보드 커스텀 서비스 @@ -61,8 +62,15 @@ public CustomKeyboard saveCustomKeyboard(CustomKeyboardRequest request, UUID use var switchProduct = getProduct(request.getSwitchId()); var keyCapProduct = getProduct(request.getKeyCapId()); + /// 악세사리 (선택) + String accessoryId = null; + if (request.getAccessoryId() != null && !request.getAccessoryId().isBlank()) { + var accessoryProduct = getProduct(request.getAccessoryId()); + accessoryId = accessoryProduct.getId(); + } + /// 객체 생성 - var customKeyboard = CustomKeyboard.of(userId, request.getLayout(), frameProduct.getId(), switchProduct.getId(), keyCapProduct.getId(), request.getName(), "thumbnail"); + var customKeyboard = CustomKeyboard.of(userId, request.getLayout(), frameProduct.getId(), switchProduct.getId(), keyCapProduct.getId(), accessoryId, request.getName(), "thumbnail"); /// 저장 후 리턴 return port.saveCustomKeyboard(customKeyboard); @@ -102,12 +110,16 @@ public Slice getCustomKeyboards(UUID userId) { } /// 필요한 모든 productId를 한 번에 수집 - Set allProductIds = new HashSet<>(); - keyboards.forEach(k -> { - allProductIds.add(k.getFrameId()); - allProductIds.add(k.getSwitchId()); - allProductIds.add(k.getKeyCapId()); - }); + Set allProductIds = keyboards.stream() + .flatMap(k -> Stream.of( + k.getFrameId(), + k.getSwitchId(), + k.getKeyCapId(), + k.getAccessoryId() // null 포함 가능, 아래에서 안전 처리 + )) + .filter(Objects::nonNull) // null 제거 + .collect(Collectors.toSet()); + /// productId로 한 번에 조회 (N+1 방지 핵심) Map productMap = getProductsByIds(allProductIds); @@ -118,8 +130,9 @@ public Slice getCustomKeyboards(UUID userId) { var frameProduct = productMap.get(keyboard.getFrameId()); var switchProduct = productMap.get(keyboard.getSwitchId()); var keyCapProduct = productMap.get(keyboard.getKeyCapId()); + var accessoryProduct = productMap.get(keyboard.getAccessoryId()); - return CustomKeyboardWithName.of(keyboard, frameProduct, switchProduct, keyCapProduct); + return CustomKeyboardWithName.of(keyboard, frameProduct, switchProduct, keyCapProduct, accessoryProduct); }) .toList(); @@ -144,8 +157,18 @@ public CustomKeyboardWithName getCustomKeyboard(Long customKeyboardId) { var switchProduct = getProduct(keyBoard.getSwitchId()); var keyCapProduct = getProduct(keyBoard.getKeyCapId()); + String accessoryId = null; + if (keyBoard.getAccessoryId() != null && !keyBoard.getAccessoryId().isBlank()) { + + } + + // 악세사리가 존재하면 조회 + var accessoryProduct = keyBoard.getAccessoryId() != null && !keyBoard.getAccessoryId().isBlank() + ? getProduct(keyBoard.getAccessoryId()) + : null; + /// 응답 - return CustomKeyboardWithName.of(keyBoard, frameProduct, switchProduct, keyCapProduct); + return CustomKeyboardWithName.of(keyBoard, frameProduct, switchProduct, keyCapProduct, accessoryProduct); } @@ -160,10 +183,6 @@ public CustomKeyboardWithName getCustomKeyboard(Long customKeyboardId) { public Slice getCustomProducts(UUID userId, String categoryId, CustomKeyboardLayout type, Pageable pageable) { Slice products; - log.info("userId : {}", userId); - log.info("categoryId : {}", categoryId); - log.info("type : {}", type); - log.info("pageable : {}", pageable); /// 타입을 바탕으로 조회하기 /// 하우징인 경우 @@ -181,6 +200,27 @@ else if (categoryId.equals(CategoryType.KEYCAP.getValue())) { return toProductListResponse(userId, categoryId, products); } + + /** + * 키보드 배열을 바탕으로 가능한 상품 개수 조회 + * + */ + @Override + public Long getCustomProductCounts(String categoryId, CustomKeyboardLayout type) { + + /// 타입을 바탕으로 조회하기 + /// 하우징인 경우 + if (categoryId.equals(CategoryType.HOUSING.getValue())){ + return productPort.getProductsAndCategoryByType(type.getDb(), categoryId); + } + /// 키캡인 경우 + else if (categoryId.equals(CategoryType.KEYCAP.getValue())) { + return productPort.getProductsByCategoryAndCustom(categoryId); + } else { + return productPort.getProductsCount(categoryId); + } + } + // ================= // 삭제 함수 // ================= diff --git a/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboard.java b/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboard.java index 916d11a..3a03b7f 100644 --- a/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboard.java +++ b/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboard.java @@ -24,6 +24,8 @@ public class CustomKeyboard extends BaseDomain { private String keyCapId; + private String accessoryId; + private String name; private String imageUrl; @@ -31,7 +33,7 @@ public class CustomKeyboard extends BaseDomain { private CustomKeyboardLayout layout; /// 정적 팩토리 메서드 - public static CustomKeyboard of(UUID userId, CustomKeyboardLayout layout, String frameId, String switchId, String keyCapId, String name, String imageUrl) { + public static CustomKeyboard of(UUID userId, CustomKeyboardLayout layout, String frameId, String switchId, String keyCapId, String accessoryId, String name, String imageUrl) { return CustomKeyboard.builder() .userId(userId) @@ -39,6 +41,7 @@ public static CustomKeyboard of(UUID userId, CustomKeyboardLayout layout, String .frameId(frameId) .switchId(switchId) .keyCapId(keyCapId) + .accessoryId(accessoryId == null ? "" : accessoryId) .name(name) .imageUrl(imageUrl) .build(); diff --git a/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardWithName.java b/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardWithName.java index 7b9648a..f951e28 100644 --- a/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardWithName.java +++ b/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardWithName.java @@ -35,15 +35,25 @@ public record CustomKeyboardWithName( String switchName, String keyCapId, String keyCapName, + String accessoryId, + String accessoryName, String name, double totalPrice, String imageUrl ) { - /// 정적 팩토리 메서드 - public static CustomKeyboardWithName of(CustomKeyboard keyboard, Product housingProduct, Product switchProduct, Product keyCapProduct) { + public static CustomKeyboardWithName of( + CustomKeyboard keyboard, + Product housingProduct, + Product switchProduct, + Product keyCapProduct, + Product accessoryProduct) { + // accessoryProduct가 null이면 가격에 더하지 않음 double totalPrice = housingProduct.getPrice() + switchProduct.getPrice() + keyCapProduct.getPrice(); + if (accessoryProduct != null) { + totalPrice += accessoryProduct.getPrice(); + } return CustomKeyboardWithName.builder() .id(keyboard.getId()) @@ -55,6 +65,8 @@ public static CustomKeyboardWithName of(CustomKeyboard keyboard, Product housing .switchName(switchProduct.getName()) .keyCapId(keyCapProduct.getId()) .keyCapName(keyCapProduct.getName()) + .accessoryId(accessoryProduct != null ? accessoryProduct.getId() : null) + .accessoryName(accessoryProduct != null ? accessoryProduct.getName() : null) .name(keyboard.getName()) .totalPrice(totalPrice) .imageUrl(keyboard.getImageUrl()) diff --git a/src/main/java/site/kikihi/custom/platform/domain/product/Product.java b/src/main/java/site/kikihi/custom/platform/domain/product/Product.java index f32470f..3346c28 100644 --- a/src/main/java/site/kikihi/custom/platform/domain/product/Product.java +++ b/src/main/java/site/kikihi/custom/platform/domain/product/Product.java @@ -25,6 +25,8 @@ public class Product { private String thumbnail; // + private String mapping; + private String manufacturer; // private String detailPageUrl; //