diff --git a/api/src/main/resources/db/migration/v10_gallery_covering_idx.sql b/api/src/main/resources/db/migration/v10_gallery_covering_idx.sql new file mode 100644 index 00000000..4aa6605c --- /dev/null +++ b/api/src/main/resources/db/migration/v10_gallery_covering_idx.sql @@ -0,0 +1,6 @@ +CREATE INDEX gallery_idx_bookmark ON gallery(bookmarked_count) +CREATE INDEX gallery_idx_update_order ON gallery(update_order) +CREATE UNIQUE INDEX gallery_idx_survey_id ON gallery(survey_id) +CREATE INDEX gallery_idx_job ON gallery(job) +CREATE INDEX gallery_idx_created_at ON gallery(created_at) +CREATE INDEX gallery_idx_updated_at ON gallery(updated_at) diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt index 3c7f33d6..4e02e8f0 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt @@ -1,10 +1,16 @@ package me.nalab.gallery.app +import me.nalab.gallery.domain.Gallery import me.nalab.gallery.domain.GalleryService +import me.nalab.gallery.domain.response.GalleriesDto import me.nalab.gallery.domain.response.GalleryDto import me.nalab.survey.application.port.`in`.web.findfeedback.FeedbackFindUseCase import me.nalab.survey.application.port.`in`.web.survey.find.SurveyFindUseCase import me.nalab.survey.application.port.`in`.web.target.find.TargetFindUseCase +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.stereotype.Service @Service @@ -23,4 +29,32 @@ class GalleryGetApp( return toGalleryDto(gallery, target, survey, feedbacks) } + + fun getGalleries(job: String, page: Int, count: Int, orderType: String): GalleriesDto { + val pageable = getPage(page, count, orderType) + val galleries = galleryService.getGalleries(job, pageable) + + val galleryDtos = toGalleryDtos(galleries) + + return GalleriesDto(galleries.totalPages, galleryDtos) + } + + private fun toGalleryDtos(galleries: Page): List { + return galleries.asSequence() + .map { gallery -> + val target = targetFindUseCase.findTarget(gallery.getTargetId()) + val survey = surveyFindUseCase.getSurveyByTargetId(gallery.getTargetId()) + val feedbacks = feedbackFindUseCase.findAllFeedbackDtoBySurveyId(survey.id) + + toGalleryDto(gallery, target, survey, feedbacks) + }.toList() + } + + private fun getPage(page: Int, count: Int, orderType: String): Pageable { + return when (orderType.lowercase()) { + "update" -> PageRequest.of(page, count, Sort.by("updateOrder").descending()) + "job" -> PageRequest.of(page, count, Sort.by("survey.bookmarkedCount").descending()) + else -> throw IllegalArgumentException("orderType 은 update와 bookmark중 하나여야 합니다. 현재 orderType \"$orderType\"") + } + } } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt index 915e2862..be01c988 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt @@ -4,6 +4,7 @@ import me.nalab.gallery.app.GalleryGetApp import me.nalab.gallery.app.GalleryPreviewApp import me.nalab.gallery.app.GalleryRegisterApp import me.nalab.gallery.controller.request.GalleryRegisterRequest +import me.nalab.gallery.domain.response.GalleriesDto import me.nalab.gallery.domain.response.GalleryDto import me.nalab.gallery.domain.response.GalleryPreviewDto import org.springframework.http.HttpStatus @@ -36,4 +37,15 @@ class GalleryController( fun getGallery(@RequestAttribute("logined") targetId: Long): GalleryDto = galleryGetApp.getGalleryByTargetId(targetId) + @GetMapping + @ResponseStatus(HttpStatus.OK) + fun getGalleries( + @RequestParam(name = "job", defaultValue = "all") job: String, + @RequestParam(name = "page", defaultValue = "0") page: Int, + @RequestParam(name = "count", defaultValue = "5") count: Int, + @RequestParam(name = "order-type", defaultValue = "update") orderType: String + ): GalleriesDto { + return galleryGetApp.getGalleries(job, page, count, orderType) + } + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt index 508f51f0..96db38a2 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt @@ -1,5 +1,7 @@ package me.nalab.gallery.domain +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query @@ -11,4 +13,8 @@ interface GalleryRepository : JpaRepository { @Lock(LockModeType.OPTIMISTIC) @Query("select g from Gallery as g where g.target.targetId = :targetId") fun findByTargetIdOrNull(@Param("targetId") targetId: Long): Gallery? + + @Query("select g from Gallery g where g.target.job in :job") + fun findGalleries(@Param("job") job: List, pageable: Pageable): Page + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt index e98c79c8..78f2610c 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt @@ -1,5 +1,7 @@ package me.nalab.gallery.domain +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -27,4 +29,13 @@ class GalleryService( fun increaseBookmarkCount(targetId: Long) { galleryRepository.findByTargetIdOrNull(targetId)?.increaseBookmarkedCount() } + + fun getGalleries(job: String, pageable: Pageable): Page { + val jobs = when (job) { + "all" -> Job.entries.toList() + else -> listOf(Job.valueOf(job.uppercase())) + } + + return galleryRepository.findGalleries(jobs, pageable) + } } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt new file mode 100644 index 00000000..33cf3f0d --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt @@ -0,0 +1,6 @@ +package me.nalab.gallery.domain.response + +data class GalleriesDto( + val totalPage: Int, + val galleries: List +) diff --git a/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt index 177ade82..39aec20b 100644 --- a/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt +++ b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt @@ -1,5 +1,6 @@ package me.nalab.gallery.app +import me.nalab.core.time.TimeUtil import me.nalab.survey.application.common.feedback.dto.* import me.nalab.survey.application.common.survey.dto.* import java.time.Instant @@ -23,7 +24,7 @@ fun surveyDto( choiceFormQuestionDto(), shortFormQuestionDto() ), - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), ): SurveyDto { return SurveyDto.builder() .id(id) @@ -36,7 +37,7 @@ fun surveyDto( fun choiceFormQuestionDto( id: Long = 0L, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), type: ChoiceFormQuestionDtoType = ChoiceFormQuestionDtoType.TENDENCY, choices: List = listOf(choiceDto()), maxSelectableCount: Int = 5, @@ -67,7 +68,7 @@ fun choiceDto( fun shortFormQuestionDto( id: Long = 0L, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), type: ShortFormQuestionDtoType = ShortFormQuestionDtoType.CUSTOM, title: String = "제가 고쳐야할점을 알려주세요.", order: Int = 1, @@ -85,7 +86,7 @@ fun shortFormQuestionDto( fun feedbackDto( id: Long = 0L, surveyId: Long = 0L, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), formQuestionFeedbackDtos: List = listOf( choiceFormQuestionFeedbackDto(), shortFormQuestionFeedbackDto() @@ -136,7 +137,7 @@ fun shortFormQuestionFeedbackDto( fun bookmarkDto( isBookmarked: Boolean = false, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), ): BookmarkDto { return BookmarkDto.builder() .isBookmarked(isBookmarked) diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt index 2728cd14..b716c905 100644 --- a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt @@ -1,5 +1,6 @@ package me.nalab.gallery.domain +import me.nalab.core.time.TimeUtil import java.time.Instant fun gallery( @@ -8,7 +9,7 @@ fun gallery( job: Job = Job.OTHERS, surveyId: Long = 0L, bookmarkedCount: Int = 0, - updateOrder: Instant = Instant.now(), + updateOrder: Instant = TimeUtil.toInstant(), ): Gallery = Gallery( id = id, targetId = targetId, diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt index d19a2551..d6ae45eb 100644 --- a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt @@ -3,11 +3,19 @@ package me.nalab.gallery.domain import io.kotest.assertions.throwables.shouldThrowMessage import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.equality.FieldsEqualityCheckConfig +import io.kotest.matchers.equality.shouldBeEqualToComparingFields import io.kotest.matchers.equality.shouldBeEqualUsingFields +import io.kotest.matchers.equals.shouldBeEqual +import me.nalab.core.time.TimeUtil import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.context.ContextConfiguration +import java.time.temporal.ChronoUnit +import kotlin.reflect.full.memberProperties @DataJpaTest @EnableJpaRepositories @@ -16,13 +24,24 @@ import org.springframework.test.context.ContextConfiguration @ContextConfiguration(classes = [GalleryService::class]) internal class GalleryServiceTest( private val galleryService: GalleryService, + private val galleryRepository: GalleryRepository, ) : DescribeSpec({ - beforeSpec { - galleryService.registerGallery(gallery(id = EXIST_GALLERY_ID, targetId = EXIST_TARGET_ID, surveyId = EXIST_SURVEY_ID)) + afterEach { + galleryRepository.deleteAll() } describe("registerGallery 메소드는") { + beforeEach { + galleryService.registerGallery( + gallery( + id = EXIST_GALLERY_ID, + targetId = EXIST_TARGET_ID, + surveyId = EXIST_SURVEY_ID + ) + ) + } + context("Gallery에 등록되지 않은 target의 Gallery를 입력받으면,") { val gallery = gallery(targetId = NOT_EXIST_TARGET_ID) @@ -43,6 +62,94 @@ internal class GalleryServiceTest( } } } + + describe("getGalleries 메소드는") { + beforeEach { + galleryService.registerGallery(oldDesignerGallery) + galleryService.registerGallery(midDeveloperGallery) + galleryService.registerGallery(latestPmGallery) + } + + context("job으로 all과 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(latestPmGallery, midDeveloperGallery, oldDesignerGallery) + + it("update순으로 정렬된 Gallery를 반환한다.") { + val result = galleryService.getGalleries("all", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 designer와 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(oldDesignerGallery) + + it("update순으로 정렬된 designer Gallery를 반환한다.") { + val result = galleryService.getGalleries("designer", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 pm과 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(latestPmGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("pm", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 developer와 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(midDeveloperGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("developer", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 all과 bookmark순으로 정렬된 Pageable을 받으면,") { + val expected = listOf(oldDesignerGallery, midDeveloperGallery, latestPmGallery) + + it("bookmark순으로 정렬된 Gallery를 반환한다") { + val result = galleryService.getGalleries("all", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 designer와 bookmark순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(oldDesignerGallery) + + it("update순으로 정렬된 designer Gallery를 반환한다.") { + val result = galleryService.getGalleries("designer", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 pm과 bookmark순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(latestPmGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("pm", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 developer와 bookmark순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(midDeveloperGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("developer", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + } }) { companion object { @@ -50,5 +157,46 @@ internal class GalleryServiceTest( private const val EXIST_GALLERY_ID = 100L private const val EXIST_TARGET_ID = 100L private const val EXIST_SURVEY_ID = 100L + + val updatePage: PageRequest = PageRequest.of(0, 5, Sort.by("updateOrder").descending()) + val bookmarkPage: PageRequest = + PageRequest.of(0, 5, Sort.by("survey.bookmarkedCount").descending()) + + val oldDesignerGallery = gallery( + id = 1, + targetId = 101, + surveyId = 101, + job = Job.DESIGNER, + updateOrder = TimeUtil.toInstant().minus(1, ChronoUnit.DAYS), + bookmarkedCount = 3 + ) + + val midDeveloperGallery = gallery( + id = 2, + targetId = 102, + surveyId = 102, + job = Job.DEVELOPER, + updateOrder = TimeUtil.toInstant(), + bookmarkedCount = 2 + ) + + val latestPmGallery = gallery( + id = 3, + targetId = 103, + surveyId = 103, + job = Job.PM, + updateOrder = TimeUtil.toInstant().plus(1, ChronoUnit.DAYS), + bookmarkedCount = 1 + ) + + private fun List.shouldBeExactlyEqualToComparingFields(galleries: List) { + this.size shouldBeEqual galleries.size + for (i in galleries.indices) { + this[i].shouldBeEqualToComparingFields( + galleries[i], + FieldsEqualityCheckConfig(propertiesToExclude = Gallery::class.memberProperties.filter { it.name == "createdAt" || it.name == "updatedAt" }) + ) + } + } } }