diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt index 094c495f..4633d647 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt @@ -4,6 +4,7 @@ import com.wafflestudio.csereal.common.aop.AuthenticatedStaff import com.wafflestudio.csereal.core.notice.dto.* import com.wafflestudio.csereal.core.notice.service.NoticeService import jakarta.validation.Valid +import org.springframework.data.domain.PageRequest import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @@ -18,18 +19,19 @@ class NoticeController( fun searchNotice( @RequestParam(required = false) tag: List?, @RequestParam(required = false) keyword: String?, - @RequestParam(required = false, defaultValue = "0") pageNum: Long + @RequestParam(required = false, defaultValue = "1") pageNum: Int, + @RequestParam(required = false, defaultValue = "false") usePageBtn: Boolean ): ResponseEntity { - return ResponseEntity.ok(noticeService.searchNotice(tag, keyword, pageNum)) + val pageSize = 20 + val pageRequest = PageRequest.of(pageNum - 1, pageSize) + return ResponseEntity.ok(noticeService.searchNotice(tag, keyword, pageRequest, usePageBtn)) } @GetMapping("/{noticeId}") fun readNotice( - @PathVariable noticeId: Long, - @RequestParam(required = false) tag: List?, - @RequestParam(required = false) keyword: String?, + @PathVariable noticeId: Long ): ResponseEntity { - return ResponseEntity.ok(noticeService.readNotice(noticeId, tag, keyword)) + return ResponseEntity.ok(noticeService.readNotice(noticeId)) } @AuthenticatedStaff @@ -63,6 +65,7 @@ class NoticeController( ) { noticeService.unpinManyNotices(request.idList) } + @DeleteMapping fun deleteManyNotices( @RequestBody request: NoticeIdListRequest diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt index 97821a0c..4a0cb84a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt @@ -7,28 +7,42 @@ import com.wafflestudio.csereal.core.notice.database.QNoticeEntity.noticeEntity import com.wafflestudio.csereal.core.notice.database.QNoticeTagEntity.noticeTagEntity import com.wafflestudio.csereal.core.notice.dto.NoticeSearchDto import com.wafflestudio.csereal.core.notice.dto.NoticeSearchResponse +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Component +import java.time.LocalDateTime +import kotlin.math.ceil interface NoticeRepository : JpaRepository, CustomNoticeRepository { fun findAllByIsImportant(isImportant: Boolean): List + fun findFirstByCreatedAtLessThanOrderByCreatedAtDesc(timestamp: LocalDateTime): NoticeEntity? + fun findFirstByCreatedAtGreaterThanOrderByCreatedAtAsc(timestamp: LocalDateTime): NoticeEntity? } interface CustomNoticeRepository { - fun searchNotice(tag: List?, keyword: String?, pageNum: Long): NoticeSearchResponse - fun findPrevNextId(noticeId: Long, tag: List?, keyword: String?): Array? + fun searchNotice( + tag: List?, + keyword: String?, + pageable: Pageable, + usePageBtn: Boolean + ): NoticeSearchResponse } @Component class NoticeRepositoryImpl( private val queryFactory: JPAQueryFactory, ) : CustomNoticeRepository { - override fun searchNotice(tag: List?, keyword: String?, pageNum: Long): NoticeSearchResponse { + override fun searchNotice( + tag: List?, + keyword: String?, + pageable: Pageable, + usePageBtn: Boolean + ): NoticeSearchResponse { val keywordBooleanBuilder = BooleanBuilder() val tagsBooleanBuilder = BooleanBuilder() if (!keyword.isNullOrEmpty()) { - val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) keywordList.forEach { if (it.length == 1) { @@ -39,8 +53,8 @@ class NoticeRepositoryImpl( .or(noticeEntity.description.contains(it)) ) } - } + } } if (!tag.isNullOrEmpty()) { tag.forEach { @@ -50,18 +64,27 @@ class NoticeRepositoryImpl( } } - val jpaQuery = queryFactory.select(noticeEntity).from(noticeEntity) + val jpaQuery = queryFactory.selectFrom(noticeEntity) .leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) - .where(keywordBooleanBuilder).where(tagsBooleanBuilder) + .where(keywordBooleanBuilder, tagsBooleanBuilder) - val countQuery = jpaQuery.clone() - val total = countQuery.select(noticeEntity.countDistinct()).fetchOne() + val total: Long + var pageRequest = pageable + + if (usePageBtn) { + val countQuery = jpaQuery.clone() + total = countQuery.select(noticeEntity.countDistinct()).fetchOne()!! + pageRequest = exchangePageRequest(pageable, total) + } else { + total = (10 * pageable.pageSize).toLong() // 10개 페이지 고정 + } - val noticeEntityList = jpaQuery.orderBy(noticeEntity.isPinned.desc()) + val noticeEntityList = jpaQuery + .orderBy(noticeEntity.isPinned.desc()) .orderBy(noticeEntity.createdAt.desc()) - .offset(20 * pageNum) - .limit(20) + .offset(pageRequest.offset) + .limit(pageRequest.pageSize.toLong()) .distinct() .fetch() @@ -77,62 +100,25 @@ class NoticeRepositoryImpl( ) } - return NoticeSearchResponse(total!!, noticeSearchDtoList) - } - - override fun findPrevNextId(noticeId: Long, tag: List?, keyword: String?): Array? { - val keywordBooleanBuilder = BooleanBuilder() - val tagsBooleanBuilder = BooleanBuilder() + return NoticeSearchResponse(total, noticeSearchDtoList) - if (!keyword.isNullOrEmpty()) { - val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) - keywordList.forEach { - if (it.length == 1) { - throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") - } else { - keywordBooleanBuilder.and( - noticeEntity.title.contains(it) - .or(noticeEntity.description.contains(it)) - ) - } + } - } - } - if (!tag.isNullOrEmpty()) { - tag.forEach { - tagsBooleanBuilder.or( - noticeTagEntity.tag.name.eq(it) - ) - } - } + private fun exchangePageRequest(pageable: Pageable, total: Long): Pageable { + /** + * 요청한 페이지 번호가 기존 데이터 사이즈 초과할 경우 마지막 페이지 데이터 반환 + */ - val noticeSearchDtoList = queryFactory.select(noticeEntity).from(noticeEntity) - .leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) - .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) - .where(keywordBooleanBuilder).where(tagsBooleanBuilder) - .orderBy(noticeEntity.isPinned.desc()) - .orderBy(noticeEntity.createdAt.desc()) - .distinct() - .fetch() + val pageNum = pageable.pageNumber + val pageSize = pageable.pageSize + val requestCount = (pageNum - 1) * pageSize - val findingId = noticeSearchDtoList.indexOfFirst { it.id == noticeId } - - val prevNext: Array? - if (findingId == -1) { - prevNext = arrayOf(null, null) - } else if (findingId != 0 && findingId != noticeSearchDtoList.size - 1) { - prevNext = arrayOf(noticeSearchDtoList[findingId + 1], noticeSearchDtoList[findingId - 1]) - } else if (findingId == 0) { - if (noticeSearchDtoList.size == 1) { - prevNext = arrayOf(null, null) - } else { - prevNext = arrayOf(noticeSearchDtoList[1], null) - } - } else { - prevNext = arrayOf(null, noticeSearchDtoList[noticeSearchDtoList.size - 2]) + if (total > requestCount) { + return pageable } - return prevNext + val requestPageNum = ceil(total.toDouble() / pageNum).toInt() + return PageRequest.of(requestPageNum, pageSize) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt index f9491548..528c4f0f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt @@ -26,7 +26,8 @@ data class NoticeDto( fun of( entity: NoticeEntity, attachmentResponses: List, - prevNext: Array? + prevNotice: NoticeEntity? = null, + nextNotice: NoticeEntity? = null ): NoticeDto = entity.run { NoticeDto( id = this.id, @@ -39,10 +40,10 @@ data class NoticeDto( isPublic = this.isPublic, isPinned = this.isPinned, isImportant = this.isImportant, - prevId = prevNext?.get(0)?.id, - prevTitle = prevNext?.get(0)?.title, - nextId = prevNext?.get(1)?.id, - nextTitle = prevNext?.get(1)?.title, + prevId = prevNotice?.id, + prevTitle = prevNotice?.title, + nextId = nextNotice?.id, + nextTitle = nextNotice?.title, attachments = attachmentResponses, ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt index eb0a17db..93f028fa 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt @@ -7,6 +7,7 @@ import com.wafflestudio.csereal.core.notice.dto.* import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService import com.wafflestudio.csereal.core.user.database.UserEntity import com.wafflestudio.csereal.core.user.database.UserRepository +import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.oauth2.core.oidc.user.OidcUser @@ -17,8 +18,14 @@ import org.springframework.web.context.request.RequestContextHolder import org.springframework.web.multipart.MultipartFile interface NoticeService { - fun searchNotice(tag: List?, keyword: String?, pageNum: Long): NoticeSearchResponse - fun readNotice(noticeId: Long, tag: List?, keyword: String?): NoticeDto + fun searchNotice( + tag: List?, + keyword: String?, + pageable: Pageable, + usePageBtn: Boolean + ): NoticeSearchResponse + + fun readNotice(noticeId: Long): NoticeDto fun createNotice(request: NoticeDto, attachments: List?): NoticeDto fun updateNotice(noticeId: Long, request: NoticeDto, attachments: List?): NoticeDto fun deleteNotice(noticeId: Long) @@ -40,27 +47,25 @@ class NoticeServiceImpl( override fun searchNotice( tag: List?, keyword: String?, - pageNum: Long + pageable: Pageable, + usePageBtn: Boolean ): NoticeSearchResponse { - return noticeRepository.searchNotice(tag, keyword, pageNum) + return noticeRepository.searchNotice(tag, keyword, pageable, usePageBtn) } @Transactional(readOnly = true) - override fun readNotice( - noticeId: Long, - tag: List?, - keyword: String? - ): NoticeDto { - val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) + override fun readNotice(noticeId: Long): NoticeDto { + val notice = noticeRepository.findByIdOrNull(noticeId) ?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") if (notice.isDeleted) throw CserealException.Csereal404("삭제된 공지사항입니다.(noticeId: $noticeId)") val attachmentResponses = attachmentService.createAttachmentResponses(notice.attachments) - val prevNext = noticeRepository.findPrevNextId(noticeId, tag, keyword) + val prevNotice = noticeRepository.findFirstByCreatedAtLessThanOrderByCreatedAtDesc(notice.createdAt!!) + val nextNotice = noticeRepository.findFirstByCreatedAtGreaterThanOrderByCreatedAtAsc(notice.createdAt!!) - return NoticeDto.of(notice, attachmentResponses, prevNext) + return NoticeDto.of(notice, attachmentResponses, prevNotice, nextNotice) } @Transactional @@ -92,7 +97,7 @@ class NoticeServiceImpl( NoticeTagEntity.createNoticeTag(newNotice, tag) } - if(attachments != null) { + if (attachments != null) { attachmentService.uploadAllAttachments(newNotice, attachments) } @@ -100,7 +105,7 @@ class NoticeServiceImpl( val attachmentResponses = attachmentService.createAttachmentResponses(newNotice.attachments) - return NoticeDto.of(newNotice, attachmentResponses, null) + return NoticeDto.of(newNotice, attachmentResponses) } @@ -112,7 +117,7 @@ class NoticeServiceImpl( notice.update(request) - if(attachments != null) { + if (attachments != null) { notice.attachments.clear() attachmentService.uploadAllAttachments(notice, attachments) } else { @@ -137,7 +142,7 @@ class NoticeServiceImpl( val attachmentResponses = attachmentService.createAttachmentResponses(notice.attachments) - return NoticeDto.of(notice, attachmentResponses, null) + return NoticeDto.of(notice, attachmentResponses) } @Transactional @@ -151,15 +156,16 @@ class NoticeServiceImpl( @Transactional override fun unpinManyNotices(idList: List) { - for(noticeId in idList) { + for (noticeId in idList) { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) ?: throw CserealException.Csereal404("존재하지 않는 공지사항을 입력하였습니다.(noticeId: $noticeId)") notice.isPinned = false } } + @Transactional override fun deleteManyNotices(idList: List) { - for(noticeId in idList) { + for (noticeId in idList) { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) ?: throw CserealException.Csereal404("존재하지 않는 공지사항을 입력하였습니다.(noticeId: $noticeId)") notice.isDeleted = true