diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java new file mode 100644 index 00000000..e2d13a7f --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java @@ -0,0 +1,16 @@ +package com.dreamypatisiel.devdevdev.domain.repository.techArticle; + +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class TechArticleDto { + private final TechArticle techArticle; + private final Double score; + + public static TechArticleDto of(TechArticle techArticle, Double score) { + return new TechArticleDto(techArticle, score); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java index c874cc41..e9341776 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java @@ -6,6 +6,7 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -13,6 +14,6 @@ public interface TechArticleRepositoryCustom { Slice findBookmarkedByMemberAndCursor(Pageable pageable, Long techArticleId, BookmarkSort bookmarkSort, Member member); - SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, - Long companyId, String keyword, Float score); + SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, + Long companyId, String keyword, Double score); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index e3d18159..2b9439b4 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -9,6 +9,8 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; +import com.querydsl.core.Tuple; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; @@ -50,9 +52,9 @@ public Slice findBookmarkedByMemberAndCursor(Pageable pageable, Lon } @Override - public SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, + public SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, Long companyId, - String keyword, Float score + String keyword, Double score ) { // 키워드가 있는 경우 FULLTEXT 검색, 없는 경우 일반 조회 if (StringUtils.hasText(keyword)) { @@ -62,9 +64,9 @@ public SliceCustom findTechArticlesByCursor(Pageable pageable, Long } // 키워드 검색 - private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, + public SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, Long companyId, - String keyword, Float score + String keyword, Double score ) { // FULLTEXT 검색 조건 생성 BooleanExpression titleMatch = Expressions.booleanTemplate( @@ -87,12 +89,14 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa techArticle.contents, keyword ); - // 전체 스코어 계산 (제목 가중치 2배) + // 전체 스코어 계산 (제목 가중치 2배, 안전한 범위로 제한) NumberTemplate totalScore = Expressions.numberTemplate(Double.class, - "({0} * 2.0) + {1}", titleScore, contentsScore + "(LEAST({0}, 100000) * 2.0) + LEAST({1}, 100000)", titleScore, contentsScore ); - List contents = query.selectFrom(techArticle) + // TechArticle과 score를 함께 조회 + List results = query.select(techArticle, totalScore) + .from(techArticle) .where(titleMatch.or(contentsMatch)) .where(getCompanyIdCondition(companyId)) .where(getCursorConditionForKeywordSearch(techArticleSort, techArticleId, score, totalScore)) @@ -100,6 +104,12 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa .limit(pageable.getPageSize()) .fetch(); + // Tuple을 TechArticleDto로 변환 + List contents = results.stream() + .map(result -> TechArticleDto.of( + result.get(techArticle), result.get(totalScore))) + .toList(); + // 키워드 검색 결과 총 갯수 long totalElements = query.select(techArticle.count()) .from(techArticle) @@ -111,16 +121,21 @@ private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pa } // 일반 조회 - private SliceCustom findTechArticlesByCursorWithoutKeyword(Pageable pageable, Long techArticleId, + private SliceCustom findTechArticlesByCursorWithoutKeyword(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, Long companyId ) { - List contents = query.selectFrom(techArticle) + List results = query.selectFrom(techArticle) .where(getCursorConditionFromTechArticleSort(techArticleSort, techArticleId)) - .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .where(getCompanyIdCondition(companyId)) .orderBy(techArticleSort(techArticleSort), techArticle.id.desc()) .limit(pageable.getPageSize()) .fetch(); + // Tuple을 TechArticleDto로 변환 + List contents = results.stream() + .map(result -> TechArticleDto.of(result, null)) + .toList(); + // 기술블로그 총 갯수 long totalElements = query.select(techArticle.count()) .from(techArticle) @@ -188,13 +203,13 @@ private OrderSpecifier techArticleSort(TechArticleSort techArticleSort) { // 키워드 검색을 위한 커서 조건 생성 private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticleSort, Long techArticleId, - Float score, NumberTemplate totalScore) { + Double score, NumberTemplate totalScore) { if (ObjectUtils.isEmpty(techArticleId) || ObjectUtils.isEmpty(score)) { return null; } // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 커서 사용 - if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + if (techArticleSort == TechArticleSort.HIGHEST_SCORE || ObjectUtils.isEmpty(techArticleSort)) { return totalScore.lt(score.doubleValue()) .or(totalScore.eq(score.doubleValue()) .and(techArticle.id.lt(techArticleId))); @@ -217,13 +232,13 @@ private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticle private OrderSpecifier getOrderSpecifierForKeywordSearch(TechArticleSort techArticleSort, NumberTemplate totalScore) { // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 정렬 - if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + if (techArticleSort == TechArticleSort.HIGHEST_SCORE || ObjectUtils.isEmpty(techArticleSort)) { return totalScore.desc(); } // 다른 정렬 방식인 경우 기존 정렬 사용 return Optional.ofNullable(techArticleSort) - .orElse(TechArticleSort.HIGHEST_SCORE).getOrderSpecifierByTechArticleSort(); + .orElse(TechArticleSort.LATEST).getOrderSpecifierByTechArticleSort(); } public BooleanExpression getCompanyIdCondition(Long companyId) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java index 1d312e5c..67ed3597 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java @@ -11,6 +11,7 @@ import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -50,17 +51,19 @@ public GuestTechArticleService(TechArticlePopularScorePolicy techArticlePopularS @Override public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, - Long companyId, Float score, Authentication authentication) { + Long companyId, Double score, Authentication authentication) { // 익명 사용자 호출인지 확인 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); // 기술블로그 조회 - SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 List techArticlesResponse = techArticles.stream() - .map(TechArticleMainResponse::of) + .map(techArticle -> TechArticleMainResponse.of( + techArticle.getTechArticle(), techArticle.getScore() + )) .toList(); return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java index ea0b854e..1d3ba278 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java @@ -5,10 +5,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechArticleRecommend; import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.*; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; @@ -50,17 +47,17 @@ public MemberTechArticleService(TechArticlePopularScorePolicy techArticlePopular @Override public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, - Long companyId, Float score, Authentication authentication) { + Long companyId, Double score, Authentication authentication) { // 회원 조회 Member member = memberProvider.getMemberByAuthentication(authentication); // 기술블로그 조회 - SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 List techArticlesResponse = techArticles.stream() - .map(techArticle -> TechArticleMainResponse.of(techArticle, member)) + .map(techArticle -> TechArticleMainResponse.of(techArticle.getTechArticle(), member, techArticle.getScore())) .toList(); return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java index 475b91d1..91213745 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java @@ -11,7 +11,7 @@ public interface TechArticleService { Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, - String keyword, Long companyId, Float score, + String keyword, Long companyId, Double score, Authentication authentication); TechArticleDetailResponse getTechArticle(Long techArticleId, String anonymousMemberId, Authentication authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java index f7423c9d..d554af14 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java @@ -37,7 +37,7 @@ public ResponseEntity>> getTechArti @RequestParam(required = false) Long techArticleId, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long companyId, - @RequestParam(required = false) Float score + @RequestParam(required = false) Double score ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java index a4eb9d2b..104f9bcb 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java @@ -31,13 +31,13 @@ public class TechArticleMainResponse { public final Long popularScore; public final Boolean isLogoImage; public final Boolean isBookmarked; - public final Float score; + public final Double score; @Builder private TechArticleMainResponse(Long id, String title, String contents, String author, CompanyResponse company, LocalDate regDate, String thumbnailUrl, String techArticleUrl, Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, Long popularScore, - Boolean isLogoImage, Boolean isBookmarked, Float score) { + Boolean isLogoImage, Boolean isBookmarked, Double score) { this.id = id; this.title = title; this.contents = contents; @@ -96,7 +96,7 @@ public static TechArticleMainResponse of(TechArticle techArticle) { .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, Member member, Float score) { + public static TechArticleMainResponse of(TechArticle techArticle, Member member, Double score) { CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) @@ -117,7 +117,7 @@ public static TechArticleMainResponse of(TechArticle techArticle, Member member, .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, Float score) { + public static TechArticleMainResponse of(TechArticle techArticle, Double score) { CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) @@ -146,8 +146,8 @@ private static String getThumbnailUrl(Url thumbnailUrl, CompanyResponse companyR return thumbnailUrl.getUrl(); } - private static Float getValidScore(Float score) { - return Objects.isNull(score) || Float.isNaN(score) ? null : score; + private static Double getValidScore(Double score) { + return Objects.isNull(score) || Double.isNaN(score) ? null : score; } private static String truncateString(String contents, int maxLength) { diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java index c540a873..e3fb8a98 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java @@ -246,7 +246,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java index f7e0f6c7..a1f532a5 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java @@ -55,7 +55,7 @@ void getTechArticlesByAnonymous() throws Exception { TechArticleMainResponse response = createTechArticleMainResponse( 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", - 10L, 5L, 100L, null, 10.0f + 10L, 5L, 100L, null, 10.0 ); SliceCustom mockSlice = new SliceCustom<>( @@ -130,7 +130,7 @@ void getTechArticlesByMember() throws Exception { TechArticleMainResponse response = createTechArticleMainResponse( 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", - 10L, 5L, 100L, true, 10.0f + 10L, 5L, 100L, true, 10.0 ); SliceCustom mockSlice = new SliceCustom<>( @@ -512,7 +512,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java index 76ad5b56..183d5e82 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java @@ -510,7 +510,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java index 4a9776aa..dd806847 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java @@ -72,7 +72,7 @@ void getTechArticlesByMember() throws Exception { TechArticleMainResponse response = createTechArticleMainResponse( 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", - 10L, 5L, 100L, true, 10.0f + 10L, 5L, 100L, true, 10.0 ); SliceCustom mockSlice = new SliceCustom<>( @@ -451,7 +451,7 @@ private TechArticleMainResponse createTechArticleMainResponse(Long id, String th String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, - long commentCount, long viewCount, Boolean isBookmarked, Float score) { + long commentCount, long viewCount, Boolean isBookmarked, Double score) { return TechArticleMainResponse.builder() .id(id) .thumbnailUrl(thumbnailUrl)