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 760ce45..3efaf06 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 @@ -3,6 +3,7 @@ import site.kikihi.custom.global.response.ApiResponse; import site.kikihi.custom.global.response.page.PageRequest; import site.kikihi.custom.global.response.page.SliceResponse; +import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomCategoryType; import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomKeyboardRequest; import site.kikihi.custom.platform.adapter.in.web.dto.response.custom.CustomKeyboardLayoutResponse; import site.kikihi.custom.platform.adapter.in.web.dto.response.custom.CustomKeyboardDetailResponse; @@ -10,7 +11,6 @@ import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.adapter.in.web.swagger.CustomKeyboardControllerSpec; import site.kikihi.custom.platform.application.in.custom.CustomKeyboardUseCase; -import site.kikihi.custom.platform.application.in.product.ProductUseCase; import site.kikihi.custom.platform.domain.custom.CustomKeyboardLayout; import site.kikihi.custom.platform.domain.custom.CustomKeyboardWithName; import site.kikihi.custom.security.oauth2.domain.PrincipalDetails; @@ -23,6 +23,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/v1/custom") @@ -31,9 +32,6 @@ public class CustomKeyboardController implements CustomKeyboardControllerSpec { private final CustomKeyboardUseCase service; - /// 상품 조회를 위한 의존성 - private final ProductUseCase productService; - /** * 커스텀 키보드 저장 * @@ -119,11 +117,14 @@ public ApiResponse> getCustomKeyboardLayout() @GetMapping("/products") public ApiResponse> getCustomKeyboardProductsByLayout( @AuthenticationPrincipal PrincipalDetails principalDetails, - @RequestParam String categoryId, + @RequestParam CustomCategoryType category, @RequestParam CustomKeyboardLayout layout, PageRequest pageRequest ) { + /// 유저가 없다면 null 저장 + UUID userId = principalDetails != null ? principalDetails.getId() : null; + /// Pageable Pageable pageable = org.springframework.data.domain.PageRequest.of( pageRequest.getPage() - 1, @@ -132,7 +133,7 @@ public ApiResponse> getCustomKeyboardProducts ); /// 서비스 - Slice content = productService.getProductsByLayout(principalDetails.getId(), categoryId, layout, pageable); + Slice content = service.getCustomProducts(userId, category.getValue(), layout, pageable); /// DTO 변경 Slice dtoSlice = new SliceImpl<>( diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/CustomCategoryTypeConverter.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/CustomCategoryTypeConverter.java new file mode 100644 index 0000000..aed74b1 --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/converter/CustomCategoryTypeConverter.java @@ -0,0 +1,27 @@ +package site.kikihi.custom.platform.adapter.in.web.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import site.kikihi.custom.global.response.ErrorCode; +import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomCategoryType; + +/** + * 파라미터 변환을 위한 컨버터입니다. + */ +@Component +public class CustomCategoryTypeConverter implements Converter { + + /** + * 문자열을 CategoryType enum으로 변환합니다. 대소문자 구분 없이 비교합니다. + * @param source the source object to convert, which must be an instance of {@code S} (never {@code null}) + */ + @Override + public CustomCategoryType convert(String source) { + for (CustomCategoryType type : CustomCategoryType.values()) { + if (type.getValue().equalsIgnoreCase(source)) { + return type; + } + } + throw new IllegalArgumentException(ErrorCode.INVALID_INPUT.getMessage()); + } +} diff --git a/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomCategoryType.java b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomCategoryType.java new file mode 100644 index 0000000..8f12949 --- /dev/null +++ b/src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/request/custom/CustomCategoryType.java @@ -0,0 +1,36 @@ +package site.kikihi.custom.platform.adapter.in.web.dto.request.custom; + + +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 enum CustomCategoryType { + + @Schema(description = "하우징(프레임)", example = "housing") + HOUSING("housing"), + + @Schema(description = "스위치", example = "switch") + SWITCH("switch"), + + @Schema(description = "키캡", example = "keycap") + KEYCAP("keycap"), + + @Schema(description = "액세서리(기타)", example = "accessory") + ACCESSORY("accessory"), + ; + + private final String value; + + @JsonValue + public String getValue() { + return value; + } +} 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 986d7a2..fd94283 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 @@ -3,6 +3,7 @@ import site.kikihi.custom.global.response.ApiResponse; import site.kikihi.custom.global.response.page.PageRequest; import site.kikihi.custom.global.response.page.SliceResponse; +import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomCategoryType; import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomKeyboardRequest; import site.kikihi.custom.platform.adapter.in.web.dto.response.custom.CustomKeyboardLayoutResponse; import site.kikihi.custom.platform.adapter.in.web.dto.response.custom.CustomKeyboardDetailResponse; @@ -93,7 +94,7 @@ ApiResponse> getMyCustoms( ) ApiResponse> getCustomKeyboardProductsByLayout( @AuthenticationPrincipal PrincipalDetails principalDetails, - @RequestParam String categoryId, + @RequestParam CustomCategoryType category, @RequestParam CustomKeyboardLayout layout, PageRequest pageRequest ); @@ -110,10 +111,10 @@ ApiResponse deleteCustomKeyboard( String REQUEST = """ { - "layout" : "PERCENT_75", - "housingId" : "686bd26d34c3c12ea9b8e7e2", + "layout" : "PERCENT_60", + "housingId" : "68b3f4fedc26d32d8881fdff", "switchId" : "686bd26d34c3c12ea9b8e7e3", - "keyCapId" : "686bd26d89b5df14c6125f58", + "keyCapId" : "68b3f653dc26d32d8881fe3d", "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 94845d8..1b48ee6 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 @@ -127,6 +127,26 @@ public Map getProductsByIds(List productIds) { .collect(Collectors.toMap(Product::getId, Function.identity())); } + /** + * 커스텀에서 사용할 함수 + * + * @param type 조회할 타입 + * @param pageable 페이징 + */ + @Override + public Slice getProductsAndCategoryByType(String type, String categoryId, Pageable pageable) { + + return documentRepository.findByTypeAndCategoryAndIsCustomTrue(type, categoryId, pageable) + .map(ProductDocument::toDomain); + } + + @Override + public Slice getProductsByCategoryAndCustom(String categoryId, Pageable pageable) { + return documentRepository.findByCategoryAndIsCustomTrue(categoryId, pageable) + .map(ProductDocument::toDomain); + } + + /** * 상품 개수 조회 * @param category 카테고리 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 24807c2..3e7c869 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 @@ -50,6 +50,12 @@ public class ProductDocument { @Builder.Default private Map specTable = new java.util.HashMap<>(); + @Field("type") + private String type; + + @Field("is_custom") + private boolean isCustom; + /// 도메인 변경 public Product toDomain(){ return Product.builder() 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 2eea375..9ac68dd 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 @@ -65,6 +65,31 @@ Slice findByCategoryAndManufacturerAndPriceRange( Pageable pageable ); + + @Query(""" + { + 'type': ?0, + 'category': ?1, + 'is_custom': true + } + """) + Slice findByTypeAndCategoryAndIsCustomTrue( + String type, + String category, + Pageable pageable + ); + + @Query(""" + { + 'category': ?0, + 'is_custom': true + } + """) + Slice findByCategoryAndIsCustomTrue( + String category, + Pageable pageable + ); + /// 아이디 기반 조회 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 d39013f..6f0e135 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 @@ -1,6 +1,8 @@ package site.kikihi.custom.platform.application.in.custom; +import org.springframework.data.domain.Pageable; import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomKeyboardRequest; +import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.domain.custom.CustomKeyboard; import site.kikihi.custom.platform.domain.custom.CustomKeyboardLayout; import site.kikihi.custom.platform.domain.custom.CustomKeyboardWithName; @@ -35,7 +37,8 @@ public interface CustomKeyboardUseCase { // 나의 커스텀 키보드 목록 조회 CustomKeyboardWithName getCustomKeyboard(Long customKeyboardId); - // 부품 추가하기 + // 배열에 맞는 상품 조회하기 + Slice getCustomProducts(UUID userId, String categoryId, CustomKeyboardLayout type, Pageable pageable); /// 삭제 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 a1bdfc2..e88403a 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 @@ -48,6 +48,10 @@ public interface ProductPort { /// 상품 ID들 바탕으로 조회 Map getProductsByIds(List productIds); + Slice getProductsAndCategoryByType(String type, String categoryId, Pageable pageable); + + Slice getProductsByCategoryAndCustom(String categoryId, Pageable pageable); + /// 상품 개수 조회 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 249d959..e6171fc 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 @@ -1,11 +1,17 @@ package site.kikihi.custom.platform.application.service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; import site.kikihi.custom.global.response.ErrorCode; import site.kikihi.custom.platform.adapter.in.web.dto.request.custom.CustomKeyboardRequest; +import site.kikihi.custom.platform.adapter.in.web.dto.request.product.CategoryType; +import site.kikihi.custom.platform.adapter.in.web.dto.response.product.ProductListResponse; import site.kikihi.custom.platform.application.in.custom.CustomKeyboardUseCase; +import site.kikihi.custom.platform.application.out.bookmark.BookmarkPort; import site.kikihi.custom.platform.application.out.custom.CustomKeyboardPort; import site.kikihi.custom.platform.application.out.product.ProductPort; import site.kikihi.custom.platform.application.out.user.UserPort; +import site.kikihi.custom.platform.domain.bookmark.Bookmark; import site.kikihi.custom.platform.domain.custom.CustomKeyboard; import site.kikihi.custom.platform.domain.custom.CustomKeyboardLayout; import site.kikihi.custom.platform.domain.custom.CustomKeyboardWithName; @@ -18,11 +24,13 @@ import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.stream.Collectors; /** * 키보드 커스텀 서비스 */ +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -30,10 +38,11 @@ public class CustomKeyboardService implements CustomKeyboardUseCase { /// DB 포트 private final CustomKeyboardPort port; + private final ProductPort productPort; /// 외부 의존성 처리 private final UserPort userPort; - private final ProductPort productPort; + private final BookmarkPort bookmarkPort; // ================= // 저장 함수 @@ -73,6 +82,11 @@ public List getKeyboardLayouts() { return CustomKeyboardLayout.getKeyboardLayouts(); } + + /** + * 내가 만든 키보드 목록 조회 + * @param userId 유저 ID + */ @Override public Slice getCustomKeyboards(UUID userId) { @@ -135,6 +149,38 @@ public CustomKeyboardWithName getCustomKeyboard(Long customKeyboardId) { } + /** + * 키보드 배열을 바탕으로 가능한 상품 목록 조회 + * + * @param userId 유저ID + * @param type 조회할 배열 타입 + * @param pageable 페이징 + */ + @Override + 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); + + /// 타입을 바탕으로 조회하기 + /// 하우징인 경우 + if (categoryId.equals(CategoryType.HOUSING.getValue())){ + products = productPort.getProductsAndCategoryByType(type.getDb(), categoryId, pageable); + } + /// 키캡인 경우 + else if (categoryId.equals(CategoryType.KEYCAP.getValue())) { + products = productPort.getProductsByCategoryAndCustom(categoryId, pageable); + } else { + products = productPort.getProducts(categoryId, pageable); + } + + /// 북마크 여부도 파악하기 + return toProductListResponse(userId, categoryId, products); + } + // ================= // 삭제 함수 // ================= @@ -201,4 +247,44 @@ private CustomKeyboard getKeyboard(Long customKeyboardId) { .orElseThrow(() -> new NoSuchElementException(ErrorCode.CUSTOM_NOT_FOUND.getMessage())); } + // ================= + // 공통 함수 + // ================= + /** + * 상품 목록 조회를 진행할때, 북마크 여부를 파악하는 함수입니다. + * @param userId 유저 ID + * @param categoryId 카테고리 ID + * @param products 상품 목록 + */ + private Slice toProductListResponse(UUID userId, String categoryId, Slice products) { + /// 응답 값 + List dtoList; + + /// 상품 목록 꺼내서 DTO 변환 + List content = products.getContent(); + + /// 로그인 하지 않은 유저가 확인한다면 + if (userId == null) { + + /// 하트가 전부 false 되는 로직 + dtoList = ProductListResponse.from(content); + + /// 새로운 Slice 객체로 생성 + return new SliceImpl<>(dtoList, products.getPageable(), products.hasNext()); + } + + /// 유저가 북마크를 했는지 체크 + List bookmarks = bookmarkPort.getBookmarksByUserIdAndCategoryId(userId, categoryId); + + /// 북마크된 상품 ID만 추출 + Set bookmarkedProductIds = bookmarks.stream() + .map(Bookmark::getProductId) + .collect(Collectors.toSet()); + + // 북마크 여부 반영하여 DTO 변환 + dtoList = ProductListResponse.from(content, bookmarkedProductIds); + + return new SliceImpl<>(dtoList, products.getPageable(), products.hasNext()); + } + } diff --git a/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardLayout.java b/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardLayout.java index a28d770..8f196ad 100644 --- a/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardLayout.java +++ b/src/main/java/site/kikihi/custom/platform/domain/custom/CustomKeyboardLayout.java @@ -7,13 +7,14 @@ @RequiredArgsConstructor public enum CustomKeyboardLayout { - PERCENT_60("60배열","미니멀의 정점"), - PERCENT_75("75배열","컴팩트한 사이즈의 깔끔함"), - TENKEYLESS("텐키리스","넘버패드없이 깔끔한 키보드"), - FULLSIZE("풀배열","원조의 품격"); + PERCENT_60("60배열", "미니멀의 정점", "60키"), + PERCENT_75("75배열", "컴팩트한 사이즈의 깔끔함", "75키"), + TENKEYLESS("텐키리스", "넘버패드없이 깔끔한 키보드", "tenkeyless"), + FULLSIZE("풀배열", "원조의 품격", "fullsize"); private final String name; private final String description; + private final String db; /// 리스트 목록 조회 public static List getKeyboardLayouts() { @@ -31,5 +32,10 @@ public String getDescription() { return description; } + @JsonIgnore + public String getDb() { + return db; + } + } diff --git a/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java index 4d43a23..34497c4 100644 --- a/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/site/kikihi/custom/security/jwt/filter/JwtAuthenticationFilter.java @@ -120,6 +120,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) { return false; } + /// 커스텀은 회원/비회원 구분해야되기에 필터를 타도록 설정 + if (request.getRequestURI().startsWith("/api/v1/custom")) { + return false; + } + /// null 인 것 해결 return requestMatcherHolder.getRequestMatchersByMinRole(null) .matches(request); diff --git a/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java b/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java index 355b71d..7b115ca 100644 --- a/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java +++ b/src/main/java/site/kikihi/custom/security/jwt/filter/RequestMatcherHolder.java @@ -44,6 +44,13 @@ public class RequestMatcherHolder { new RequestInfo(DELETE, "/api/v1/search/**", Role.USER), new RequestInfo(PUT, "/api/v1/search/**", Role.USER), + // 커스텀 관련 + new RequestInfo(GET, "/api/v1/custom/products", null), + new RequestInfo(GET, "/api/v1/custom/layout", null), + new RequestInfo(GET, "/api/v1/custom/**", Role.USER), + new RequestInfo(POST, "/api/v1/custom/**", Role.USER), + new RequestInfo(DELETE, "/api/v1/custom/**", Role.USER), + // static resources new RequestInfo(GET, "/docs/**", null), new RequestInfo(GET, "/*.ico", null),