From 7bd223530474a8596ff4561c255758a01aaacba9 Mon Sep 17 00:00:00 2001 From: Minseo Kang Date: Thu, 14 Nov 2024 21:52:43 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=ED=81=AC=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B0=BE=EA=B8=B0=20API=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Controller 작성 * :art: 응답 dto 추가 * :art: 서비스 인터페이스 작성 * :art: 서비스 로직 오버라이드 * :art: 응답 구조 변경 * :sparkles: Pageable 매개변수 추가 * :art: 응답 구조 변경 * :construction: 응답 확인 * :art: 작업 가능 필드 응답 변경 * :sparkles: 최소 비용 응답에 포함 * :sparkles: 가격 조건 추가 * :art: 응답 dto에 포트폴리오 썸네일 이미지 경로 추가 * :sparkles: 가격순 정렬 추가 * :sparkles: 카테고리 정렬 기능 추가 * :memo: Swagger 문서 업데이트 (응답) * :memo: 가능한 작업 카테고리 예시 추가 * :memo: Swagger 문서 업데이트(Parameter) * :art: 작업가능 필드 응답 한글로 변경 --- .../creator/controller/CreatorController.java | 22 +++++- .../controller/CreatorControllerApi.java | 75 ++++++++++++++++++- .../creator/dto/filtering/CreatorInfo.java | 52 +++++++++++++ .../creator/service/CreatorService.java | 4 + .../creator/service/CreatorServiceImpl.java | 51 ++++++++++++- .../repository/UserRepository.java | 27 +++++-- .../util/format/WorkTypeConverter.java | 2 +- .../util/response/PageResponse.java | 16 ++++ 8 files changed, 239 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/example/gather_back_end/creator/dto/filtering/CreatorInfo.java diff --git a/src/main/java/org/example/gather_back_end/creator/controller/CreatorController.java b/src/main/java/org/example/gather_back_end/creator/controller/CreatorController.java index 5960e0a..bab2f07 100644 --- a/src/main/java/org/example/gather_back_end/creator/controller/CreatorController.java +++ b/src/main/java/org/example/gather_back_end/creator/controller/CreatorController.java @@ -1,26 +1,31 @@ package org.example.gather_back_end.creator.controller; +import java.util.List; import lombok.RequiredArgsConstructor; import org.example.gather_back_end.bucket.service.BucketService; import org.example.gather_back_end.creator.dto.CreateCreatorReq; import org.example.gather_back_end.creator.dto.GetCreatorRes; +import org.example.gather_back_end.creator.dto.filtering.CreatorInfo; import org.example.gather_back_end.creator.service.CreatorService; import org.example.gather_back_end.domain.User; import org.example.gather_back_end.portfolio.service.PortfolioService; import org.example.gather_back_end.repository.UserRepository; import org.example.gather_back_end.util.jwt.dto.CustomOAuth2User; +import org.example.gather_back_end.util.response.PageResponse; import org.example.gather_back_end.util.response.SuccessResponse; import org.example.gather_back_end.work.service.WorkService; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.util.List; @RestController @RequiredArgsConstructor @@ -88,4 +93,19 @@ public SuccessResponse getCreatorInfo(Authentication authentication) { return SuccessResponse.of(getCreatorRes); } + // 크리에이터 찾기 + @GetMapping("/filtering") + public SuccessResponse> filteringCreator( + Authentication authentication, + @PageableDefault(size = 12, page = 0) Pageable pageable, + @RequestParam(value = "price", required = false) Integer price, + @RequestParam(value = "category", required = false) String category, + @RequestParam(value = "align", defaultValue = "recently", required = false) String recently + ) { + CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal(); + String providerId = customOAuth2User.getUsername(); + PageResponse res = creatorService.filteringCreator(providerId, pageable, price, category, recently); + return SuccessResponse.of(res); + } + } diff --git a/src/main/java/org/example/gather_back_end/creator/controller/CreatorControllerApi.java b/src/main/java/org/example/gather_back_end/creator/controller/CreatorControllerApi.java index 8200c98..ac78268 100644 --- a/src/main/java/org/example/gather_back_end/creator/controller/CreatorControllerApi.java +++ b/src/main/java/org/example/gather_back_end/creator/controller/CreatorControllerApi.java @@ -1,6 +1,7 @@ package org.example.gather_back_end.creator.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,16 +9,21 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.example.gather_back_end.creator.dto.CreateCreatorReq; +import org.example.gather_back_end.creator.dto.filtering.CreatorInfo; +import org.example.gather_back_end.util.response.PageResponse; import org.example.gather_back_end.util.response.SuccessResponse; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import java.util.List; -@Tag(name = "크리에이터 프로필 관련", description = "크리에이터 프로필 관련된 API") +@Tag(name = "크리에이터 관련", description = "크리에이터 관련된 API") public interface CreatorControllerApi { @Operation(summary = "크리에이터 등록 완료") @@ -77,4 +83,71 @@ SuccessResponse createCreator( @GetMapping SuccessResponse getCreatorInfo(Authentication authentication); + @Operation(summary = "크리에이터 찾기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "크리에이터 찾기에서 사용되는 API", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "timestamp": "2024-11-14T21:39:55.407616", + "isSuccess": true, + "code": "200", + "message": "호출에 성공하였습니다.", + "data": { + "totalPage": 1, + "totalElements": 3, + "pagingSize": 12, + "currentPage": 1, + "isFirst": true, + "isLast": true, + "isEmpty": false, + "data": [ + { + "nickname": "USER4255", + "availableWork": [ + "SNS" + ], + "introductionTitle": "sds", + "startPrice": "49000", + "thumbnailImgUrl": null + }, + { + "nickname": "USER3322", + "availableWork": [ + "인쇄물", + "SNS" + ], + "introductionTitle": "dkssudd", + "startPrice": "500", + "thumbnailImgUrl": "ㅇㅇ" + }, + { + "nickname": "USER3333", + "availableWork": [ + "SNS" + ], + "introductionTitle": "dfdfdfdfdfdfdfdf", + "startPrice": "1000", + "thumbnailImgUrl": null + } + ] + } + } + """ + ), + schema = @Schema(implementation = SuccessResponse.class))) + }) + @GetMapping + SuccessResponse> filteringCreator( + Authentication authentication, + @PageableDefault(size = 12, page = 0) Pageable pageable, + @Parameter(description = "가격 필터링 기준 (선택)", example = "50000") + @RequestParam(value = "price", required = false) Integer price, + @Parameter(description = "카테고리 필터링 기준 (선택): PRINTS, VIDEO, SNS_POST 중 하나", example = "PRINTS") + @RequestParam(value = "category", required = false) String category, + @Parameter(description = "정렬 기준 (선택): recently, lowPrice, highPrice 중 하나, 기본값은 recently", example = "lowPrice") + @RequestParam(value = "align", defaultValue = "recently", required = false) String recently + ); + } diff --git a/src/main/java/org/example/gather_back_end/creator/dto/filtering/CreatorInfo.java b/src/main/java/org/example/gather_back_end/creator/dto/filtering/CreatorInfo.java new file mode 100644 index 0000000..3f419b9 --- /dev/null +++ b/src/main/java/org/example/gather_back_end/creator/dto/filtering/CreatorInfo.java @@ -0,0 +1,52 @@ +package org.example.gather_back_end.creator.dto.filtering; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import org.example.gather_back_end.domain.Portfolio; +import org.example.gather_back_end.domain.User; +import org.example.gather_back_end.domain.Work; +import org.example.gather_back_end.util.format.WorkTypeConverter; + +public record CreatorInfo( + @Schema(description = "크리에이터명", example = "hello") + String nickname, + + @Schema(description = "가능한 작업 카테고리", example = "[\"SNS\", \"인쇄물\", \"비디오\"]") + List availableWork, + + @Schema(description = "크리에이터 소개글 제목", example = "안녕하세요") + String introductionTitle, + + @Schema(description = "작업 시작 가격", example = "5000") + String startPrice, + + @Schema(description = "포트폴리오 썸네일 주소", example = "dfdfd") + String thumbnailImgUrl +) { + + public static CreatorInfo from(User user, List availableWork, List portfolioList) { + + // workList에서 startPrice 중 가장 작은 값 찾기 + String minStartPrice = user.getWorkList().stream() + .map(Work::getStartPrice) + .min(Comparator.naturalOrder()) + .map(String::valueOf) // int를 String으로 변환 + .orElse("N/A"); // workList가 비어 있을 경우 기본값 + + // availableWork를 한글명으로 변환 + List translatedAvailableWork = user.getWorkList().stream() + .map(work -> WorkTypeConverter.toKorean(work.getCategory())) + .distinct() + .collect(Collectors.toList()); + + return new CreatorInfo( + user.getNickname(), + translatedAvailableWork, + user.getIntroductionTitle(), + minStartPrice, + portfolioList.getFirst().getThumbnailImgUrl() + ); + } +} diff --git a/src/main/java/org/example/gather_back_end/creator/service/CreatorService.java b/src/main/java/org/example/gather_back_end/creator/service/CreatorService.java index 3c617de..9a23e61 100644 --- a/src/main/java/org/example/gather_back_end/creator/service/CreatorService.java +++ b/src/main/java/org/example/gather_back_end/creator/service/CreatorService.java @@ -1,6 +1,9 @@ package org.example.gather_back_end.creator.service; import org.example.gather_back_end.creator.dto.GetCreatorRes; +import org.example.gather_back_end.creator.dto.filtering.CreatorInfo; +import org.example.gather_back_end.util.response.PageResponse; +import org.springframework.data.domain.Pageable; import org.springframework.security.core.Authentication; public interface CreatorService { @@ -17,4 +20,5 @@ void createCreator( GetCreatorRes getCreator(String nickname); GetCreatorRes getCreatorInfo(Authentication authentication); + PageResponse filteringCreator(String providerId, Pageable pageable, Integer price, String category, String align); } diff --git a/src/main/java/org/example/gather_back_end/creator/service/CreatorServiceImpl.java b/src/main/java/org/example/gather_back_end/creator/service/CreatorServiceImpl.java index 587b03a..1852c49 100644 --- a/src/main/java/org/example/gather_back_end/creator/service/CreatorServiceImpl.java +++ b/src/main/java/org/example/gather_back_end/creator/service/CreatorServiceImpl.java @@ -1,20 +1,33 @@ package org.example.gather_back_end.creator.service; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.example.gather_back_end.creator.dto.GetCreatorRes; +import org.example.gather_back_end.creator.dto.filtering.CreatorInfo; import org.example.gather_back_end.domain.User; +import org.example.gather_back_end.domain.WorkType; import org.example.gather_back_end.portfolio.dto.GetPortfolioRes; import org.example.gather_back_end.repository.PortfolioRepository; import org.example.gather_back_end.repository.UserRepository; import org.example.gather_back_end.repository.WorkRepository; import org.example.gather_back_end.util.jwt.dto.CustomOAuth2User; +import org.example.gather_back_end.util.response.PageResponse; import org.example.gather_back_end.work.dto.GetWorkRes; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; -import java.util.List; -@RequiredArgsConstructor +@Slf4j @Service +@RequiredArgsConstructor public class CreatorServiceImpl implements CreatorService { private final UserRepository userRepository; @@ -89,4 +102,38 @@ public GetCreatorRes getCreatorInfo(Authentication authentication){ } + @Override + public PageResponse filteringCreator(String providerId, Pageable pageable, Integer price, String category, String align) { + + Sort sort = switch (align) { + case "lowPrice" -> Sort.by(Sort.Direction.ASC, "workList.startPrice"); + case "highPrice" -> Sort.by(Sort.Direction.DESC, "workList.startPrice"); + default -> Sort.by(Sort.Direction.DESC, "createAt"); + }; + + Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); + + // category를 WorkType으로 변환, 값이 없는 경우 null로 설정 + WorkType workCategory = (category != null) ? WorkType.valueOf(category) : null; + + Page creators = userRepository.customFiltering(price, workCategory, sortedPageable); + + Set seenIds = new LinkedHashSet<>(); + List creatorInfoList = creators.getContent().stream() + .filter(user -> seenIds.add(user.getId())) + .map(user -> CreatorInfo.from( + user, + user.getWorkList().stream() + .map(work -> work.getCategory().name()) + .toList(), + user.getPortfolioList() + )) + .collect(Collectors.toList()); + + PageImpl res = new PageImpl<>(creatorInfoList, sortedPageable, creatorInfoList.size()); + + return PageResponse.of(res); + } + + } diff --git a/src/main/java/org/example/gather_back_end/repository/UserRepository.java b/src/main/java/org/example/gather_back_end/repository/UserRepository.java index e949143..5bd7dda 100644 --- a/src/main/java/org/example/gather_back_end/repository/UserRepository.java +++ b/src/main/java/org/example/gather_back_end/repository/UserRepository.java @@ -3,7 +3,10 @@ import java.util.List; import java.util.Optional; import org.example.gather_back_end.domain.User; +import org.example.gather_back_end.domain.WorkType; import org.example.gather_back_end.user.exception.UserNotFoundException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -21,6 +24,7 @@ public interface UserRepository extends JpaRepository { default User getById(Long id) { return findById(id).orElseThrow(UserNotFoundException::new); } + default User getByUsername(String username) { return findByUsername(username).orElseThrow(UserNotFoundException::new); } @@ -35,15 +39,28 @@ default User getByNickname(String nickname) { // 크리에이터 찾기 @Query("SELECT u FROM User u " + - "JOIN u.workList w " + - "JOIN u.portfolioList p " + - "WHERE u.introductionTitle IS NOT NULL " + // 소개글 제목 존재 - "AND SIZE(u.workList) > 0 " + // 작업 가능 항목 등록 - "AND SIZE(u.portfolioList) > 0 ") // 포트폴리오 등록 + "JOIN u.workList w " + + "JOIN u.portfolioList p " + + "WHERE u.introductionTitle IS NOT NULL " + // 소개글 제목 존재 + "AND SIZE(u.workList) > 0 " + // 작업 가능 항목 등록 + "AND SIZE(u.portfolioList) > 0 ") + // 포트폴리오 등록 // TODO: 포트폴리오 더미데이터 모두 넣은 후 주석 해제 // "AND SIZE(u.portfolioList) > 0 " + // 포트폴리오 등록 // "AND p.thumbnailImgUrl IS NOT NULL " + // 포트폴리오 썸네일 존재 // "AND p.fileUrl IS NOT NULL") // 포트폴리오 파일 존재 List findAllCreators(); + @Query("SELECT DISTINCT u FROM User u " + + "JOIN u.workList w " + + "WHERE u.introductionTitle IS NOT NULL " + + "AND (:price IS NULL OR " + + " (:price = 10000 AND w.startPrice < 10000) OR " + + " (:price = 50000 AND w.startPrice < 50000) OR " + + " (:price = 100000 AND w.startPrice < 100000) OR " + + " (:price = 200000 AND w.startPrice < 200000) OR " + + " (:price = 200001 AND w.startPrice >= 200000)) " + + "AND (:category IS NULL OR w.category = :category)") + Page customFiltering(@Param("price") Integer price, @Param("category") WorkType category, Pageable pageable); + } diff --git a/src/main/java/org/example/gather_back_end/util/format/WorkTypeConverter.java b/src/main/java/org/example/gather_back_end/util/format/WorkTypeConverter.java index 5770543..5c3887a 100644 --- a/src/main/java/org/example/gather_back_end/util/format/WorkTypeConverter.java +++ b/src/main/java/org/example/gather_back_end/util/format/WorkTypeConverter.java @@ -7,7 +7,7 @@ public class WorkTypeConverter { private static final Map workTypeToKoreanMap = Map.of( WorkType.PRINTS, "인쇄물", - WorkType.SNS_POST, "SNS 게시물", + WorkType.SNS_POST, "SNS", WorkType.VIDEO, "영상" ); diff --git a/src/main/java/org/example/gather_back_end/util/response/PageResponse.java b/src/main/java/org/example/gather_back_end/util/response/PageResponse.java index f9b6edc..55a3c88 100644 --- a/src/main/java/org/example/gather_back_end/util/response/PageResponse.java +++ b/src/main/java/org/example/gather_back_end/util/response/PageResponse.java @@ -4,6 +4,7 @@ * 게시글 등의 리스트 조회 시 필요한 응답 클래스 * Pageable 이용 */ +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import lombok.Builder; import lombok.Getter; @@ -13,13 +14,28 @@ @Builder public class PageResponse { + @Schema(description = "총 페이지 수", example = "1") private int totalPage; + + @Schema(description = "결과 데이터 개수", example = "3") private Long totalElements; + + @Schema(description = "한 번에 가져올 데이터 개수", example = "12") private int pagingSize; + + @Schema(description = "현재 페이지 (1부터 시작)", example = "1") private int currentPage; + + @Schema(description = "첫 번째 페이지인지 여부", example = "true") private Boolean isFirst; + + @Schema(description = "마지막 페이지인지 여부", example = "false") private Boolean isLast; + + @Schema(description = "결과 데이터가 비어있는지 여부", example = "false") private Boolean isEmpty; + + @Schema(description = "실제 데이터가 담기는 필드") private List data; public static PageResponse of(Page page) {