diff --git a/src/main/java/com/listywave/common/util/StringUtils.java b/src/main/java/com/listywave/common/util/StringUtils.java new file mode 100644 index 00000000..3d8aab10 --- /dev/null +++ b/src/main/java/com/listywave/common/util/StringUtils.java @@ -0,0 +1,10 @@ +package com.listywave.common.util; + +import java.util.regex.Pattern; + +public class StringUtils { + + public static boolean match(String source, String keyword) { + return source.matches(".*" + Pattern.quote(keyword) + ".*"); + } +} diff --git a/src/main/java/com/listywave/list/application/domain/Lists.java b/src/main/java/com/listywave/list/application/domain/Lists.java index 7f4b4783..7c816da2 100644 --- a/src/main/java/com/listywave/list/application/domain/Lists.java +++ b/src/main/java/com/listywave/list/application/domain/Lists.java @@ -1,5 +1,7 @@ package com.listywave.list.application.domain; +import static com.listywave.common.util.StringUtils.match; + import com.listywave.list.application.dto.ListCreateCommand; import com.listywave.list.application.vo.ItemComment; import com.listywave.list.application.vo.ItemImageUrl; @@ -163,6 +165,32 @@ public static Lists createList( return lists; } + public boolean isRelatedWith(String keyword) { + if (keyword.isBlank()) { + return true; + } + if (match(title.getValue(), keyword)) { + return true; + } + if (labels.stream().anyMatch(label -> match(label.getLabelName(), keyword))) { + return true; + } + if (items.stream().anyMatch(item -> match(item.getTitle(), keyword) || match(item.getComment(), keyword))) { + return true; + } + return false; + } + + public boolean isIncluded(CategoryType category) { + if (category.equals(CategoryType.ENTIRE)) { + return true; + } + if (this.category.equals(category)) { + return true; + } + return false; + } + public void sortItems() { this.getItems().sort(Comparator.comparing(Item::getRanking)); } diff --git a/src/main/java/com/listywave/list/application/domain/SortType.java b/src/main/java/com/listywave/list/application/domain/SortType.java new file mode 100644 index 00000000..1ee6fc87 --- /dev/null +++ b/src/main/java/com/listywave/list/application/domain/SortType.java @@ -0,0 +1,10 @@ +package com.listywave.list.application.domain; + +public enum SortType { + + NEW, + OLD, + RELATED, + COLLECTED, + ; +} diff --git a/src/main/java/com/listywave/list/application/dto/response/ListSearchResponse.java b/src/main/java/com/listywave/list/application/dto/response/ListSearchResponse.java new file mode 100644 index 00000000..d58ca97b --- /dev/null +++ b/src/main/java/com/listywave/list/application/dto/response/ListSearchResponse.java @@ -0,0 +1,79 @@ +package com.listywave.list.application.dto.response; + +import com.listywave.list.application.domain.Item; +import com.listywave.list.application.domain.Lists; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record ListSearchResponse( + List resultLists, + Long totalCount, + Long cursorId, + boolean hasNext +) { + + public static ListSearchResponse of(java.util.List lists, Long totalCount, Long cursorId, boolean hasNext) { + return ListSearchResponse.builder() + .resultLists(ListInfo.toList(lists)) + .totalCount(totalCount) + .cursorId(cursorId) + .hasNext(hasNext) + .build(); + } +} + +@Builder +record ListInfo( + Long id, + String title, + java.util.List items, + boolean isPublic, + String backgroundColor, + LocalDateTime updatedDate, + Long ownerId, + String ownerNickname, + String ownerProfileImageUrl +) { + + public static List toList(java.util.List lists) { + return lists.stream() + .map(ListInfo::of) + .toList(); + } + + private static ListInfo of(Lists lists) { + return ListInfo.builder() + .id(lists.getId()) + .title(lists.getTitle()) + .items(ItemInfo.toList(lists.getItems())) + .isPublic(lists.isPublic()) + .backgroundColor(lists.getBackgroundColor()) + .updatedDate(lists.getUpdatedDate()) + .ownerId(lists.getUser().getId()) + .ownerNickname(lists.getUser().getNickname()) + .ownerProfileImageUrl(lists.getUser().getProfileImageUrl()) + .build(); + } +} + +@Builder +record ItemInfo( + Long id, + String title +) { + + public static java.util.List toList(java.util.List items) { + return items.stream() + .map(ItemInfo::of) + .toList(); + } + + private static ItemInfo of(Item item) { + return ItemInfo.builder() + .id(item.getId()) + .title(item.getTitle()) + .build(); + } +} diff --git a/src/main/java/com/listywave/list/application/service/ListService.java b/src/main/java/com/listywave/list/application/service/ListService.java index 728ba692..a7b7403d 100644 --- a/src/main/java/com/listywave/list/application/service/ListService.java +++ b/src/main/java/com/listywave/list/application/service/ListService.java @@ -1,5 +1,8 @@ package com.listywave.list.application.service; +import static com.listywave.list.application.domain.SortType.COLLECTED; +import static com.listywave.list.application.domain.SortType.OLD; + import com.listywave.auth.application.domain.JwtManager; import com.listywave.collaborator.application.domain.Collaborator; import com.listywave.collaborator.repository.CollaboratorRepository; @@ -7,13 +10,16 @@ import com.listywave.common.exception.ErrorCode; import com.listywave.common.util.UserUtil; import com.listywave.image.application.service.ImageService; +import com.listywave.list.application.domain.CategoryType; import com.listywave.list.application.domain.Comment; import com.listywave.list.application.domain.Item; import com.listywave.list.application.domain.Lists; +import com.listywave.list.application.domain.SortType; import com.listywave.list.application.dto.ListCreateCommand; import com.listywave.list.application.dto.response.ListCreateResponse; import com.listywave.list.application.dto.response.ListDetailResponse; import com.listywave.list.application.dto.response.ListRecentResponse; +import com.listywave.list.application.dto.response.ListSearchResponse; import com.listywave.list.application.dto.response.ListTrandingResponse; import com.listywave.list.presentation.dto.request.ItemCreateRequest; import com.listywave.list.repository.CommentRepository; @@ -192,4 +198,52 @@ public ListRecentResponse getRecentLists(String accessToken) { private boolean isSignedIn(String accessToken) { return !accessToken.isBlank(); } + + // TODO: 관련도 순 추가 (List 일급 컬렉션 만들어서 Scoring 하는 방식) + // TODO: 리팩터링 + public ListSearchResponse search(String keyword, SortType sortType, CategoryType category, int size, Long cursorId) { + List all = listRepository.findAll(); + + List filtered = all.stream() + .filter(list -> list.isIncluded(category)) + .filter(list -> list.isRelatedWith(keyword)) + .sorted((list, other) -> { + if (sortType.equals(OLD)) { + return list.getUpdatedDate().compareTo(other.getUpdatedDate()); + } + if (sortType.equals(COLLECTED)) { + return -(list.getCollectCount() - other.getCollectCount()); + } + return -(list.getUpdatedDate().compareTo(other.getUpdatedDate())); + }) + .toList(); + + List result; + if (cursorId == 0L) { + if (filtered.size() >= size) { + return ListSearchResponse.of(filtered.subList(0, size), (long) filtered.size(), filtered.get(size - 1).getId(), true); + } + return ListSearchResponse.of(filtered, (long) filtered.size(), filtered.get(filtered.size() - 1).getId(), false); + } else { + Lists cursorList = listRepository.getById(cursorId); + + int cursorIndex = filtered.indexOf(cursorList); + int startIndex = cursorIndex + 1; + int endIndex = cursorIndex + 1 + size; + + if (endIndex >= filtered.size()) { + endIndex = filtered.size() - 1; + } + + result = filtered.subList(startIndex, endIndex + 1); + } + + int totalCount = filtered.size(); + if (result.size() < size) { + return ListSearchResponse.of(result, (long) totalCount, result.get(result.size() - 1).getId(), false); + } + boolean hasNext = result.size() > size; + result = result.subList(0, size); + return ListSearchResponse.of(result, (long) totalCount, result.get(result.size() - 1).getId(), hasNext); + } } diff --git a/src/main/java/com/listywave/list/presentation/controller/ListController.java b/src/main/java/com/listywave/list/presentation/controller/ListController.java index da72a522..a632fbd8 100644 --- a/src/main/java/com/listywave/list/presentation/controller/ListController.java +++ b/src/main/java/com/listywave/list/presentation/controller/ListController.java @@ -1,9 +1,12 @@ package com.listywave.list.presentation.controller; +import com.listywave.list.application.domain.CategoryType; +import com.listywave.list.application.domain.SortType; import com.listywave.list.application.dto.ListCreateCommand; import com.listywave.list.application.dto.response.ListCreateResponse; import com.listywave.list.application.dto.response.ListDetailResponse; import com.listywave.list.application.dto.response.ListRecentResponse; +import com.listywave.list.application.dto.response.ListSearchResponse; import com.listywave.list.application.dto.response.ListTrandingResponse; import com.listywave.list.application.service.ListService; import com.listywave.list.presentation.dto.request.ListCreateRequest; @@ -18,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -67,4 +71,16 @@ ResponseEntity getRecentLists( ListRecentResponse recentLists = listService.getRecentLists(accessToken); return ResponseEntity.ok(recentLists); } + + @GetMapping("/search") + ResponseEntity search( + @RequestParam(value = "keyword", defaultValue = "") String keyword, + @RequestParam(value = "sort", defaultValue = "new") SortType sort, + @RequestParam(value = "category", defaultValue = "entire") CategoryType category, + @RequestParam(value = "size", defaultValue = "5") int size, + @RequestParam(value = "cursorId", defaultValue = "0") Long cursorId + ) { + ListSearchResponse response = listService.search(keyword, sort, category, size, cursorId); + return ResponseEntity.ok(response); + } }