Skip to content

Commit

Permalink
Merge pull request #126 from Team-WSS/feat/#124
Browse files Browse the repository at this point in the history
[FEAT] 일반 검색 API 구현
  • Loading branch information
rinarina0429 authored Aug 7, 2024
2 parents 0a3e838 + 3a78f1e commit 92f8089
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class SecurityConfig {
"/users/login",
"/users/nickname/check",
"/actuator/health",
"/novels",
"/novels/{novelId}",
"/novels/{novelId}/info",
"/novels/{novelId}/feeds",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.websoso.WSSServer.dto.novel.NovelGetResponseBasic;
import org.websoso.WSSServer.dto.novel.NovelGetResponseFeedTab;
import org.websoso.WSSServer.dto.novel.NovelGetResponseInfoTab;
import org.websoso.WSSServer.dto.novel.SearchedNovelsGetResponse;
import org.websoso.WSSServer.dto.popularNovel.PopularNovelsGetResponse;
import org.websoso.WSSServer.service.FeedService;
import org.websoso.WSSServer.service.NovelService;
Expand All @@ -32,7 +33,8 @@ public class NovelController {
private final FeedService feedService;

@GetMapping("/{novelId}")
public ResponseEntity<NovelGetResponseBasic> getNovelInfoBasic(Principal principal, @PathVariable Long novelId) {
public ResponseEntity<NovelGetResponseBasic> getNovelInfoBasic(Principal principal,
@PathVariable Long novelId) {
if (principal == null) {
return ResponseEntity
.status(OK)
Expand Down Expand Up @@ -89,6 +91,16 @@ public ResponseEntity<Void> unregisterAsInterest(Principal principal,
.build();
}

@GetMapping
public ResponseEntity<SearchedNovelsGetResponse> searchNovels(@RequestParam(required = false) String query,
@RequestParam int page,
@RequestParam int size) {

return ResponseEntity
.status(OK)
.body(novelService.searchNovels(query, page, size));
}

@GetMapping("/popular")
public ResponseEntity<PopularNovelsGetResponse> getTodayPopularNovels(Principal principal) {
//TODO 차단 관계에 있는 유저의 피드글 처리
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.websoso.WSSServer.dto.novel;

import org.websoso.WSSServer.domain.Novel;

public record NovelGetResponsePreview(
Long novelId,
String novelImage,
String title,
String author,
Long interestCount,
Float novelRating,
Long novelRatingCount
) {
public static NovelGetResponsePreview of(Novel novel, Long interestCount, Float novelRating,
Long novelRatingCount) {
return new NovelGetResponsePreview(
novel.getNovelId(),
novel.getNovelImage(),
novel.getTitle(),
novel.getAuthor(),
interestCount,
novelRating,
novelRatingCount
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.websoso.WSSServer.dto.novel;

import java.util.List;

public record SearchedNovelsGetResponse(
Long resultCount,
Boolean isLoadable,
List<NovelGetResponsePreview> novels
) {
public static SearchedNovelsGetResponse of(Long resultCount, Boolean isLoadable,
List<NovelGetResponsePreview> novels) {
return new SearchedNovelsGetResponse(
resultCount,
isLoadable,
novels
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.websoso.WSSServer.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.websoso.WSSServer.domain.Novel;

public interface NovelCustomRepository {

Page<Novel> findSearchedNovels(Pageable pageable, String query);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.websoso.WSSServer.repository;

import static org.websoso.WSSServer.domain.QNovel.novel;
import static org.websoso.WSSServer.domain.QUserNovel.userNovel;
import static org.websoso.WSSServer.domain.common.ReadStatus.WATCHED;
import static org.websoso.WSSServer.domain.common.ReadStatus.WATCHING;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.core.types.dsl.StringTemplate;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.websoso.WSSServer.domain.Novel;
import org.websoso.WSSServer.domain.QNovel;

@Repository
@RequiredArgsConstructor
public class NovelCustomRepositoryImpl implements NovelCustomRepository {

private final JPAQueryFactory jpaQueryFactory;

@Override
public Page<Novel> findSearchedNovels(Pageable pageable, String query) {

String searchQuery = query.replaceAll("\\s+", "");

BooleanExpression titleContainsQuery = getSpaceRemovedString(novel.title).containsIgnoreCase(searchQuery);
BooleanExpression authorContainsQuery = getSpaceRemovedString(novel.author).containsIgnoreCase(searchQuery);

List<Novel> novelsByTitle = jpaQueryFactory
.selectFrom(novel)
.leftJoin(novel.userNovels, userNovel)
.where(titleContainsQuery)
.groupBy(novel.novelId)
.orderBy(getPopularity(novel).desc())
.fetch();

List<Novel> novelsByAuthor = jpaQueryFactory
.selectFrom(novel)
.leftJoin(novel.userNovels, userNovel)
.where(authorContainsQuery.and(titleContainsQuery.not()))
.groupBy(novel.novelId)
.orderBy(getPopularity(novel).desc())
.fetch();

List<Novel> result = Stream
.concat(novelsByTitle.stream(), novelsByAuthor.stream())
.toList();

long total = result.size();
int start = (int) pageable.getOffset();
int end = Math.min((start + pageable.getPageSize()), (int) total);

return new PageImpl<>(result.subList(start, end), pageable, total);
}

private StringTemplate getSpaceRemovedString(StringPath stringPath) {
return Expressions.stringTemplate(
"REPLACE(REPLACE({0}, ' ', ''), CHAR(9), '')",
stringPath
);
}

private NumberExpression<Long> getPopularity(QNovel novel) {
return new CaseBuilder()
.when(userNovel.isInterest.isTrue()
.or(userNovel.status.in(WATCHING, WATCHED)))
.then(1L)
.otherwise(0L)
.sum();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import org.websoso.WSSServer.domain.Novel;

@Repository
public interface NovelRepository extends JpaRepository<Novel, Long> {
public interface NovelRepository extends JpaRepository<Novel, Long>, NovelCustomRepository {

@Query("SELECT n FROM Novel n JOIN UserNovel un ON n.novelId = un.novel.novelId " +
"GROUP BY n.novelId " +
Expand Down
57 changes: 55 additions & 2 deletions src/main/java/org/websoso/WSSServer/service/NovelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.websoso.WSSServer.domain.Avatar;
Expand All @@ -31,6 +33,8 @@
import org.websoso.WSSServer.dto.keyword.KeywordCountGetResponse;
import org.websoso.WSSServer.dto.novel.NovelGetResponseBasic;
import org.websoso.WSSServer.dto.novel.NovelGetResponseInfoTab;
import org.websoso.WSSServer.dto.novel.NovelGetResponsePreview;
import org.websoso.WSSServer.dto.novel.SearchedNovelsGetResponse;
import org.websoso.WSSServer.dto.platform.PlatformGetResponse;
import org.websoso.WSSServer.dto.popularNovel.PopularNovelGetResponse;
import org.websoso.WSSServer.dto.popularNovel.PopularNovelsGetResponse;
Expand Down Expand Up @@ -92,7 +96,8 @@ public NovelGetResponseBasic getNovelInfoBasic(User user, Long novelId) {
}

private String getNovelGenreNames(List<NovelGenre> novelGenres) {
return novelGenres.stream().map(novelGenre -> novelGenre.getGenre().getGenreName())
return novelGenres.stream()
.map(novelGenre -> novelGenre.getGenre().getGenreName())
.collect(Collectors.joining("/"));
}

Expand Down Expand Up @@ -152,7 +157,8 @@ public NovelGetResponseInfoTab getNovelInfoInfoTab(Long novelId) {
}

private List<PlatformGetResponse> getPlatforms(Novel novel) {
return novelPlatformRepository.findAllByNovel(novel).stream().map(PlatformGetResponse::of)
return novelPlatformRepository.findAllByNovel(novel).stream()
.map(PlatformGetResponse::of)
.collect(Collectors.toList());
}

Expand Down Expand Up @@ -237,6 +243,53 @@ private List<KeywordCountGetResponse> getKeywords(Novel novel) {
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
public SearchedNovelsGetResponse searchNovels(String query, int page, int size) {

PageRequest pageRequest = PageRequest.of(page, size);

if (query.isBlank()) {
return SearchedNovelsGetResponse.of(0L, false, Collections.emptyList());
}

Page<Novel> novels = novelRepository.findSearchedNovels(pageRequest, query);

List<NovelGetResponsePreview> novelGetResponsePreviews = novels.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());

return SearchedNovelsGetResponse.of(novels.getTotalElements(), novels.hasNext(), novelGetResponsePreviews);
}

private NovelGetResponsePreview convertToDTO(Novel novel) {

List<UserNovel> userNovels = novel.getUserNovels();

long interestCount = userNovels.stream()
.filter(UserNovel::getIsInterest)
.count();

long novelRatingCount = userNovels.stream()
.filter(un -> un.getUserNovelRating() != 0.0f)
.count();

double novelRatingSum = userNovels.stream()
.filter(un -> un.getUserNovelRating() != 0.0f)
.mapToDouble(UserNovel::getUserNovelRating)
.sum();

Float novelRatingAverage = novelRatingCount == 0
? 0.0f
: Math.round((float) (novelRatingSum / novelRatingCount) * 10.0f) / 10.0f;

return NovelGetResponsePreview.of(
novel,
interestCount,
novelRatingAverage,
novelRatingCount
);
}

@Transactional(readOnly = true)
public PopularNovelsGetResponse getTodayPopularNovels() {
List<Long> novelIdsFromPopularNovel = getNovelIdsFromPopularNovel();
Expand Down

0 comments on commit 92f8089

Please sign in to comment.