diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt index f9b1a73a..79c58a43 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt @@ -7,6 +7,9 @@ import com.wafflestudio.csereal.core.news.service.NewsService import com.wafflestudio.csereal.core.user.database.Role import com.wafflestudio.csereal.core.user.database.UserRepository import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Positive +import org.hibernate.validator.constraints.Length import org.springframework.data.domain.PageRequest import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -41,6 +44,15 @@ class NewsController( return ResponseEntity.ok(newsService.searchNews(tag, keyword, pageRequest, usePageBtn, isStaff)) } + @GetMapping("/totalSearch") + fun searchTotalNews( + @RequestParam(required = true) @Length(min = 1) @NotBlank keyword: String, + @RequestParam(required = true) @Positive number: Int, + @RequestParam(required = false, defaultValue = "200") @Positive stringLength: Int, + ) = ResponseEntity.ok( + newsService.searchTotalNews(keyword, number, stringLength) + ) + @GetMapping("/{newsId}") fun readNews( @PathVariable newsId: Long diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt index 76b356e2..eec400f0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt @@ -8,8 +8,13 @@ import com.wafflestudio.csereal.common.utils.FixedPageRequest import com.wafflestudio.csereal.common.utils.cleanTextFromHtml import com.wafflestudio.csereal.core.news.database.QNewsEntity.newsEntity import com.wafflestudio.csereal.core.news.database.QNewsTagEntity.newsTagEntity +import com.wafflestudio.csereal.core.news.database.QTagInNewsEntity.tagInNewsEntity import com.wafflestudio.csereal.core.news.dto.NewsSearchDto import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse +import com.wafflestudio.csereal.core.news.dto.NewsTotalSearchDto +import com.wafflestudio.csereal.core.news.dto.NewsTotalSearchElement +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity +import com.wafflestudio.csereal.core.resource.mainImage.database.QMainImageEntity.mainImageEntity import com.wafflestudio.csereal.core.notice.database.QNoticeEntity import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService import org.springframework.data.domain.Pageable @@ -24,13 +29,13 @@ interface NewsRepository : JpaRepository, CustomNewsRepository } interface CustomNewsRepository { - fun searchNews( - tag: List?, - keyword: String?, - pageable: Pageable, - usePageBtn: Boolean, - isStaff: Boolean - ): NewsSearchResponse + fun searchNews(tag: List?, keyword: String?, pageable: Pageable, usePageBtn: Boolean, isStaff: Boolean): NewsSearchResponse + fun searchTotalNews( + keyword: String, + number: Int, + amount: Int, + imageUrlCreator: (MainImageEntity?) -> String?, + ): NewsTotalSearchDto } @Component @@ -111,4 +116,64 @@ class NewsRepositoryImpl( } return NewsSearchResponse(total, newsSearchDtoList) } + + override fun searchTotalNews( + keyword: String, + number: Int, + amount: Int, + imageUrlCreator: (MainImageEntity?) -> String?, + ): NewsTotalSearchDto { + val doubleTemplate = commonRepository.searchFullDoubleTextTemplate( + keyword, + newsEntity.title, + newsEntity.plainTextDescription, + ) + + val searchResult = queryFactory.select( + newsEntity.id, + newsEntity.title, + newsEntity.date, + newsEntity.plainTextDescription, + mainImageEntity, + ).from(newsEntity) + .leftJoin(mainImageEntity) + .where(doubleTemplate.gt(0.0)) + .limit(number.toLong()) + .fetch() + + val searchResultTags = queryFactory.select( + newsTagEntity.news.id, + newsTagEntity.tag.name, + ).from(newsTagEntity) + .rightJoin(newsEntity) + .leftJoin(tagInNewsEntity) + .where(newsTagEntity.news.id.`in`(searchResult.map { it[newsEntity.id] })) + .distinct() + .fetch() + + val total = queryFactory.select(newsEntity.countDistinct()) + .from(newsEntity) + .where(doubleTemplate.gt(0.0)) + .fetchOne()!! + + return NewsTotalSearchDto( + total.toInt(), + searchResult.map { + NewsTotalSearchElement( + id = it[newsEntity.id]!!, + title = it[newsEntity.title]!!, + date = it[newsEntity.date], + tags = searchResultTags.filter { + tag -> tag[newsTagEntity.news.id] == it[newsEntity.id] + }.map { + tag -> tag[newsTagEntity.tag.name]!!.krName + }, + imageUrl = imageUrlCreator(it[mainImageEntity]), + description = it[newsEntity.plainTextDescription]!!, + keyword = keyword, + amount = amount, + ) + } + ) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsTotalSearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsTotalSearchDto.kt new file mode 100644 index 00000000..3b8e9fcf --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsTotalSearchDto.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.news.dto + +data class NewsTotalSearchDto ( + val total: Int, + val results: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsTotalSearchElement.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsTotalSearchElement.kt new file mode 100644 index 00000000..69c048ce --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsTotalSearchElement.kt @@ -0,0 +1,32 @@ +package com.wafflestudio.csereal.core.news.dto + +import com.wafflestudio.csereal.common.utils.substringAroundKeyword +import java.time.LocalDateTime + +data class NewsTotalSearchElement private constructor( + val id: Long, + val title: String, + val date: LocalDateTime?, + val tags: List, + val imageUrl: String?, +) { + lateinit var partialDescription: String + var boldStartIndex: Int = 0 + var boldEndIndex: Int = 0 + + constructor( + id: Long, + title: String, + date: LocalDateTime?, + tags: List, + imageUrl: String?, + description: String, + keyword: String, + amount: Int, + ) : this(id, title, date, tags, imageUrl) { + val (startIdx, substring) = substringAroundKeyword(keyword, description, amount) + partialDescription = substring + boldStartIndex = startIdx ?: 0 + boldEndIndex = startIdx?.plus(keyword.length) ?: 0 + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt index f8b81d82..472248e1 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt @@ -4,6 +4,7 @@ import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.news.database.* import com.wafflestudio.csereal.core.news.dto.NewsDto import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse +import com.wafflestudio.csereal.core.news.dto.NewsTotalSearchDto import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService import org.springframework.data.domain.Pageable @@ -32,6 +33,7 @@ interface NewsService { fun deleteNews(newsId: Long) fun enrollTag(tagName: String) + fun searchTotalNews(keyword: String, number: Int, amount: Int): NewsTotalSearchDto } @Service @@ -53,6 +55,18 @@ class NewsServiceImpl( return newsRepository.searchNews(tag, keyword, pageable, usePageBtn, isStaff) } + @Transactional(readOnly = true) + override fun searchTotalNews( + keyword: String, + number: Int, + amount: Int, + ) = newsRepository.searchTotalNews( + keyword, + number, + amount, + mainImageService::createImageURL, + ) + @Transactional(readOnly = true) override fun readNews(newsId: Long): NewsDto { val news: NewsEntity = newsRepository.findByIdOrNull(newsId)