From 6d371bb35654b11ac4f012988d12bd2425608204 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 29 Jun 2026 16:35:58 +0900 Subject: [PATCH] =?UTF-8?q?fix(series-banner):=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=83=9D=EC=84=B1=EC=9D=84=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A1=9C=20=EC=98=AE=EA=B8=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminContentSeriesBannerController.kt | 22 +-- .../series/main/SeriesMainController.kt | 7 +- .../main/banner/ContentSeriesBannerService.kt | 37 ++++- ...ntentSeriesBannerServiceIntegrationTest.kt | 137 ++++++++++++++++++ 4 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerServiceIntegrationTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/banner/AdminContentSeriesBannerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/banner/AdminContentSeriesBannerController.kt index 7ec2edd5..e4ec69fc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/banner/AdminContentSeriesBannerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/banner/AdminContentSeriesBannerController.kt @@ -3,9 +3,7 @@ package kr.co.vividnext.sodalive.admin.content.series.banner import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.admin.content.banner.UpdateBannerOrdersRequest -import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerListPageResponse import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerRegisterRequest -import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerResponse import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerUpdateRequest import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.ApiResponse @@ -53,13 +51,7 @@ class AdminContentSeriesBannerController( @RequestParam(defaultValue = "20") size: Int ) = run { val pageable = PageRequest.of(page, size) - val banners = bannerService.getActiveBanners(pageable) - val response = SeriesBannerListPageResponse( - totalCount = banners.totalElements, - content = banners.content.map { - SeriesBannerResponse.from(it, imageHost, appendLanguageToSeriesTitle = true) - } - ) + val response = bannerService.getActiveBanners(pageable, imageHost) ApiResponse.ok(response) } @@ -68,8 +60,7 @@ class AdminContentSeriesBannerController( */ @GetMapping("/{bannerId}") fun getBannerDetail(@PathVariable bannerId: Long) = run { - val banner = bannerService.getBannerById(bannerId) - val response = SeriesBannerResponse.from(banner, imageHost) + val response = bannerService.getBannerDetailResponse(bannerId, imageHost) ApiResponse.ok(response) } @@ -86,8 +77,7 @@ class AdminContentSeriesBannerController( val banner = bannerService.registerBanner(seriesId = request.seriesId, imagePath = "", lang = request.lang) val imagePath = saveImage(banner.id!!, image) - val updatedBanner = bannerService.updateBanner(banner.id!!, imagePath) - val response = SeriesBannerResponse.from(updatedBanner, imageHost) + val response = bannerService.updateBannerResponse(banner.id!!, imagePath, imageHost = imageHost) ApiResponse.ok(response) } @@ -104,12 +94,12 @@ class AdminContentSeriesBannerController( // 배너 존재 확인 bannerService.getBannerById(request.bannerId) val imagePath = saveImage(request.bannerId, image) - val updated = bannerService.updateBanner( + val response = bannerService.updateBannerResponse( bannerId = request.bannerId, imagePath = imagePath, - seriesId = request.seriesId + seriesId = request.seriesId, + imageHost = imageHost ) - val response = SeriesBannerResponse.from(updated, imageHost) ApiResponse.ok(response) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/SeriesMainController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/SeriesMainController.kt index 0d21dc79..3138b7bf 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/SeriesMainController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/SeriesMainController.kt @@ -1,6 +1,5 @@ package kr.co.vividnext.sodalive.content.series.main -import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerResponse import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.content.series.ContentSeriesService @@ -35,11 +34,7 @@ class SeriesMainController( if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") val preference = resolvePreference(member) - val banners = bannerService.getDisplayBanners(PageRequest.of(0, 10), langContext.lang) - .content - .map { - SeriesBannerResponse.from(it, imageHost) - } + val banners = bannerService.getDisplayBannerResponses(PageRequest.of(0, 10), langContext.lang, imageHost) val completedSeriesList = contentSeriesService.getSeriesList( creatorId = null, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerService.kt index fbce2efa..45178f53 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerService.kt @@ -1,6 +1,8 @@ package kr.co.vividnext.sodalive.content.series.main.banner import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository +import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerListPageResponse +import kr.co.vividnext.sodalive.admin.content.series.banner.dto.SeriesBannerResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.i18n.Lang import org.springframework.data.domain.Page @@ -9,23 +11,54 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service +@Transactional(readOnly = true) class ContentSeriesBannerService( private val bannerRepository: SeriesBannerRepository, private val seriesRepository: AdminContentSeriesRepository ) { - fun getActiveBanners(pageable: Pageable): Page { - return bannerRepository.findByIsActiveTrueOrderBySortOrderAsc(pageable) + fun getActiveBanners(pageable: Pageable, imageHost: String): SeriesBannerListPageResponse { + val banners = bannerRepository.findByIsActiveTrueOrderBySortOrderAsc(pageable) + return SeriesBannerListPageResponse( + totalCount = banners.totalElements, + content = banners.content.map { + SeriesBannerResponse.from( + banner = it, + imageHost = imageHost, + appendLanguageToSeriesTitle = true + ) + } + ) } fun getDisplayBanners(pageable: Pageable, lang: Lang): Page { return bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(lang, pageable) } + fun getDisplayBannerResponses(pageable: Pageable, lang: Lang, imageHost: String): List { + return getDisplayBanners(pageable, lang).content.map { + SeriesBannerResponse.from(it, imageHost) + } + } + fun getBannerById(bannerId: Long): SeriesBanner { return bannerRepository.findById(bannerId) .orElseThrow { SodaException(messageKey = "series.banner.error.not_found") } } + fun getBannerDetailResponse(bannerId: Long, imageHost: String): SeriesBannerResponse { + return SeriesBannerResponse.from(getBannerById(bannerId), imageHost) + } + + @Transactional + fun updateBannerResponse( + bannerId: Long, + imagePath: String? = null, + seriesId: Long? = null, + imageHost: String + ): SeriesBannerResponse { + return SeriesBannerResponse.from(updateBanner(bannerId, imagePath, seriesId), imageHost) + } + @Transactional fun registerBanner(seriesId: Long, imagePath: String, lang: Lang? = null): SeriesBanner { val series = seriesRepository.findByIdAndActiveTrue(seriesId) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerServiceIntegrationTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerServiceIntegrationTest.kt new file mode 100644 index 00000000..c5650943 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerServiceIntegrationTest.kt @@ -0,0 +1,137 @@ +package kr.co.vividnext.sodalive.content.series.main.banner + +import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre +import kr.co.vividnext.sodalive.creator.admin.content.series.Series +import kr.co.vividnext.sodalive.i18n.Lang +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.domain.PageRequest +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.support.TransactionTemplate +import javax.persistence.EntityManager + +@SpringBootTest( + properties = [ + "cloud.aws.cloud-front.host=https://cdn.test", + "spring.datasource.url=jdbc:h2:mem:series-banner-service-integration;" + + "MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" + ] +) +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class ContentSeriesBannerServiceIntegrationTest @Autowired constructor( + private val service: ContentSeriesBannerService, + private val entityManager: EntityManager, + private val transactionTemplate: TransactionTemplate +) { + @Test + @DisplayName("OSIV off 환경에서 관리자 시리즈 배너 목록 응답 생성 시 lazy 초기화 예외가 발생하지 않는다") + fun shouldCreateAdminSeriesBannerListResponseWhenOpenInViewIsDisabled() { + val fixtureIds = createBannerFixture(suffix = "list", sortOrder = 1) + + val response = service.getActiveBanners(PageRequest.of(0, 20), "https://cdn.test") + + assertEquals(1, response.totalCount) + assertEquals(fixtureIds.seriesId, response.content.first().seriesId) + assertEquals("series-admin-banner (일본어)", response.content.first().seriesTitle) + assertEquals("https://cdn.test/banner/jp.png", response.content.first().imagePath) + } + + @Test + @DisplayName("OSIV off 환경에서 관리자 시리즈 배너 상세 응답 생성 시 lazy 초기화 예외가 발생하지 않는다") + fun shouldCreateAdminSeriesBannerDetailResponseWhenOpenInViewIsDisabled() { + val fixtureIds = createBannerFixture(suffix = "detail", sortOrder = 2) + + val response = service.getBannerDetailResponse(fixtureIds.bannerId, "https://cdn.test") + + assertEquals(fixtureIds.bannerId, response.id) + assertEquals(fixtureIds.seriesId, response.seriesId) + assertEquals("series-admin-banner", response.seriesTitle) + assertEquals("https://cdn.test/banner/jp.png", response.imagePath) + } + + @Test + @DisplayName("OSIV off 환경에서 공개 시리즈 메인 배너 응답 생성 시 lazy 초기화 예외가 발생하지 않는다") + fun shouldCreatePublicMainSeriesBannerResponseWhenOpenInViewIsDisabled() { + val fixtureIds = createBannerFixture(suffix = "public", sortOrder = 3) + + val response = service.getDisplayBannerResponses(PageRequest.of(0, 10), Lang.JA, "https://cdn.test") + + assertEquals(1, response.size) + assertEquals(fixtureIds.seriesId, response.first().seriesId) + assertEquals("series-admin-banner", response.first().seriesTitle) + assertEquals("https://cdn.test/banner/jp.png", response.first().imagePath) + } + + @Test + @DisplayName("OSIV off 환경에서 관리자 시리즈 배너 수정 응답 생성 시 lazy 초기화 예외가 발생하지 않는다") + fun shouldCreateAdminSeriesBannerUpdateResponseWhenOpenInViewIsDisabled() { + val fixtureIds = createBannerFixture(suffix = "update", sortOrder = 4) + + val response = service.updateBannerResponse( + bannerId = fixtureIds.bannerId, + imagePath = "banner/updated.png", + imageHost = "https://cdn.test" + ) + + assertEquals(fixtureIds.bannerId, response.id) + assertEquals(fixtureIds.seriesId, response.seriesId) + assertEquals("series-admin-banner", response.seriesTitle) + assertEquals("https://cdn.test/banner/updated.png", response.imagePath) + } + + private fun createBannerFixture(suffix: String, sortOrder: Int): BannerFixtureIds { + return transactionTemplate.execute { + val creator = Member( + email = "series-admin-banner-service-$suffix@test.com", + password = "password", + nickname = "series-admin-banner-creator", + role = MemberRole.CREATOR + ) + entityManager.persist(creator) + + val genre = SeriesGenre(genre = "series-admin-banner-genre") + entityManager.persist(genre) + + val series = Series( + title = "series-admin-banner", + introduction = "introduction", + languageCode = "ko" + ).apply { + member = creator + this.genre = genre + } + entityManager.persist(series) + + val banner = SeriesBanner( + imagePath = "banner/jp.png", + series = series, + sortOrder = sortOrder, + lang = Lang.JA + ) + entityManager.persist( + banner + ) + + entityManager.flush() + val fixtureIds = BannerFixtureIds( + seriesId = series.id!!, + bannerId = banner.id!! + ) + entityManager.clear() + fixtureIds + }!! + } + + private data class BannerFixtureIds( + val seriesId: Long, + val bannerId: Long + ) +}