From 84007e1b72a883179904e3b836adb9784cb150d0 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Wed, 24 Apr 2024 23:54:39 +0900
Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EB=A6=AC=EC=A6=88=20=EB=A6=AC?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../content/series/ContentSeriesController.kt | 36 +++++++++
 .../content/series/ContentSeriesRepository.kt | 81 +++++++++++++++++++
 .../content/series/ContentSeriesService.kt    | 59 ++++++++++++++
 .../content/series/GetSeriesListRawItem.kt    | 57 +++++++++++++
 .../content/series/GetSeriesListResponse.kt   | 24 ++++++
 .../content/ContentSeriesContentRepository.kt | 65 +++++++++++++++
 .../creator/admin/content/series/Series.kt    |  4 +
 7 files changed, 326 insertions(+)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListRawItem.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt
new file mode 100644
index 0000000..f7a187b
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesController.kt
@@ -0,0 +1,36 @@
+package kr.co.vividnext.sodalive.content.series
+
+import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.common.SodaException
+import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
+import kr.co.vividnext.sodalive.member.Member
+import org.springframework.data.domain.Pageable
+import org.springframework.security.core.annotation.AuthenticationPrincipal
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@RequestMapping("/audio-content/series")
+class ContentSeriesController(private val service: ContentSeriesService) {
+    @GetMapping
+    fun getSeriesList(
+        @RequestParam creatorId: Long,
+        @RequestParam("sortType", required = false) sortType: SeriesSortType = SeriesSortType.NEWEST,
+        @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
+        pageable: Pageable
+    ) = run {
+        if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
+
+        ApiResponse.ok(
+            service.getSeriesList(
+                creatorId = creatorId,
+                sortType = sortType,
+                member = member,
+                offset = pageable.offset,
+                limit = pageable.pageSize.toLong()
+            )
+        )
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt
new file mode 100644
index 0000000..6aaa22f
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt
@@ -0,0 +1,81 @@
+package kr.co.vividnext.sodalive.content.series
+
+import com.querydsl.jpa.impl.JPAQueryFactory
+import kr.co.vividnext.sodalive.admin.content.series.genre.QSeriesGenre.seriesGenre
+import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
+import kr.co.vividnext.sodalive.creator.admin.content.series.Series
+import kr.co.vividnext.sodalive.member.QMember.member
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface ContentSeriesRepository : JpaRepository<Series, Long>, ContentSeriesQueryRepository
+
+interface ContentSeriesQueryRepository {
+    fun getSeriesTotalCount(creatorId: Long, isAuth: Boolean): Int
+    fun getSeriesRawItemList(
+        imageHost: String,
+        creatorId: Long,
+        isAuth: Boolean,
+        offset: Long,
+        limit: Long
+    ): List<GetSeriesListRawItem>
+}
+
+class ContentSeriesQueryRepositoryImpl(
+    private val queryFactory: JPAQueryFactory
+) : ContentSeriesQueryRepository {
+    override fun getSeriesTotalCount(creatorId: Long, isAuth: Boolean): Int {
+        var where = series.member.id.eq(creatorId)
+            .and(series.isActive.isTrue)
+
+        if (!isAuth) {
+            where = where.and(series.isAdult.isFalse)
+        }
+
+        return queryFactory
+            .select(series.id)
+            .from(series)
+            .where(where)
+            .fetch()
+            .size
+    }
+
+    override fun getSeriesRawItemList(
+        imageHost: String,
+        creatorId: Long,
+        isAuth: Boolean,
+        offset: Long,
+        limit: Long
+    ): List<GetSeriesListRawItem> {
+        var where = series.member.id.eq(creatorId)
+            .and(series.isActive.isTrue)
+
+        if (!isAuth) {
+            where = where.and(series.isAdult.isFalse)
+        }
+
+        return queryFactory
+            .select(
+                QGetSeriesListRawItem(
+                    series.id,
+                    series.title,
+                    series.coverImage.coalesce("profile/default-profile.png")
+                        .prepend("/")
+                        .prepend(imageHost),
+                    series.publishedDaysOfWeek,
+                    series.state,
+                    series.genre.genre,
+                    series.isAdult,
+                    series.member.id,
+                    series.member.nickname,
+                    series.member.profileImage.coalesce("profile/default-profile.png")
+                        .prepend("/")
+                        .prepend(imageHost)
+                )
+            )
+            .from(series)
+            .innerJoin(series.member, member)
+            .innerJoin(series.genre, seriesGenre)
+            .where(where)
+            .fetch()
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt
new file mode 100644
index 0000000..f7ac607
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt
@@ -0,0 +1,59 @@
+package kr.co.vividnext.sodalive.content.series
+
+import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository
+import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
+import kr.co.vividnext.sodalive.member.Member
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.time.LocalDateTime
+
+@Service
+class ContentSeriesService(
+    private val repository: ContentSeriesRepository,
+    private val seriesContentRepository: ContentSeriesContentRepository,
+
+    @Value("\${cloud.aws.cloud-front.host}")
+    private val coverImageHost: String
+) {
+    fun getSeriesList(
+        creatorId: Long,
+        member: Member,
+        sortType: SeriesSortType = SeriesSortType.NEWEST,
+        offset: Long = 0,
+        limit: Long = 10
+    ): GetSeriesListResponse {
+        val totalCount = repository.getSeriesTotalCount(creatorId = creatorId, isAuth = member.auth != null)
+        val rawItems = repository.getSeriesRawItemList(
+            imageHost = coverImageHost,
+            creatorId = creatorId,
+            isAuth = member.auth != null,
+            offset = offset,
+            limit = limit
+        )
+
+        val items = rawItems
+            .map { it.toSeriesListItem() }
+            .map {
+                it.numberOfContent = seriesContentRepository.getContentCount(
+                    seriesId = it.seriesId,
+                    isAdult = member.auth == null
+                )
+
+                it
+            }
+            .map {
+                val nowDateTime = LocalDateTime.now()
+
+                it.isNew = seriesContentRepository.isNewContent(
+                    seriesId = it.seriesId,
+                    isAdult = member.auth == null,
+                    fromDate = nowDateTime.minusDays(7),
+                    nowDate = nowDateTime
+                )
+
+                it
+            }
+
+        return GetSeriesListResponse(totalCount, items)
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListRawItem.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListRawItem.kt
new file mode 100644
index 0000000..8b65f7d
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListRawItem.kt
@@ -0,0 +1,57 @@
+package kr.co.vividnext.sodalive.content.series
+
+import com.querydsl.core.annotations.QueryProjection
+import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
+import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
+
+data class GetSeriesListRawItem @QueryProjection constructor(
+    val seriesId: Long,
+    val title: String,
+    val coverImage: String,
+    val publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>,
+    val state: SeriesState,
+    val genre: String,
+    val isAdult: Boolean,
+    val creatorId: Long,
+    val creatorNickname: String,
+    val creatorProfileImage: String
+
+) {
+    fun toSeriesListItem(): GetSeriesListResponse.SeriesListItem {
+        return GetSeriesListResponse.SeriesListItem(
+            seriesId = seriesId,
+            title = title,
+            coverImage = coverImage,
+            publishedDaysOfWeek = publishedDaysOfWeekText(),
+            isComplete = state == SeriesState.COMPLETE,
+            creator = GetSeriesListResponse.SeriesListItemCreator(
+                creatorId = creatorId,
+                nickname = creatorNickname,
+                profileImage = creatorProfileImage
+            )
+        )
+    }
+
+    private fun publishedDaysOfWeekText(): String {
+        val dayOfWeekText = publishedDaysOfWeek.toList().sortedBy { it.ordinal }
+            .map {
+                when (it) {
+                    SeriesPublishedDaysOfWeek.SUN -> "일"
+                    SeriesPublishedDaysOfWeek.MON -> "월"
+                    SeriesPublishedDaysOfWeek.TUE -> "화"
+                    SeriesPublishedDaysOfWeek.WED -> "수"
+                    SeriesPublishedDaysOfWeek.THU -> "목"
+                    SeriesPublishedDaysOfWeek.FRI -> "금"
+                    SeriesPublishedDaysOfWeek.SAT -> "토"
+                    SeriesPublishedDaysOfWeek.RANDOM -> "랜덤"
+                }
+            }
+            .joinToString(", ") { it }
+
+        return if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)) {
+            dayOfWeekText
+        } else {
+            "매주 ${dayOfWeekText}요일"
+        }
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt
new file mode 100644
index 0000000..f83b40c
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesListResponse.kt
@@ -0,0 +1,24 @@
+package kr.co.vividnext.sodalive.content.series
+
+data class GetSeriesListResponse(
+    val totalCount: Int,
+    val items: List<SeriesListItem>
+) {
+    data class SeriesListItem(
+        val seriesId: Long,
+        val title: String,
+        val coverImage: String,
+        val publishedDaysOfWeek: String,
+        val isComplete: Boolean = false,
+        val creator: SeriesListItemCreator,
+        var numberOfContent: Int = 0,
+        var isNew: Boolean = false,
+        var isPopular: Boolean = false
+    )
+
+    data class SeriesListItemCreator(
+        val creatorId: Long,
+        val nickname: String,
+        val profileImage: String
+    )
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt
new file mode 100644
index 0000000..e8f955a
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/content/ContentSeriesContentRepository.kt
@@ -0,0 +1,65 @@
+package kr.co.vividnext.sodalive.content.series.content
+
+import com.querydsl.jpa.impl.JPAQueryFactory
+import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
+import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
+import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
+import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
+import org.springframework.data.jpa.repository.JpaRepository
+import java.time.LocalDateTime
+
+interface ContentSeriesContentRepository : JpaRepository<SeriesContent, Long>, ContentSeriesContentQueryRepository
+
+interface ContentSeriesContentQueryRepository {
+    fun getContentCount(seriesId: Long, isAdult: Boolean): Int
+    fun isNewContent(seriesId: Long, isAdult: Boolean, fromDate: LocalDateTime, nowDate: LocalDateTime): Boolean
+}
+
+class ContentSeriesContentQueryRepositoryImpl(
+    private val queryFactory: JPAQueryFactory
+) : ContentSeriesContentQueryRepository {
+    override fun getContentCount(seriesId: Long, isAdult: Boolean): Int {
+        var where = seriesContent.series.id.eq(seriesId)
+            .and(seriesContent.content.isActive.isTrue)
+
+        if (!isAdult) {
+            where = where.and(seriesContent.content.isAdult.isFalse)
+        }
+
+        return queryFactory
+            .select(seriesContent.id)
+            .from(seriesContent)
+            .innerJoin(seriesContent.series, series)
+            .innerJoin(seriesContent.content, audioContent)
+            .where(where)
+            .fetch()
+            .size
+    }
+
+    override fun isNewContent(
+        seriesId: Long,
+        isAdult: Boolean,
+        fromDate: LocalDateTime,
+        nowDate: LocalDateTime
+    ): Boolean {
+        var where = seriesContent.series.id.eq(seriesId)
+            .and(seriesContent.content.isActive.isTrue)
+            .and(seriesContent.content.releaseDate.after(fromDate))
+            .and(seriesContent.content.releaseDate.before(nowDate))
+
+        if (!isAdult) {
+            where = where.and(seriesContent.content.isAdult.isFalse)
+        }
+
+        val itemCount = queryFactory
+            .select(seriesContent.id)
+            .from(seriesContent)
+            .innerJoin(seriesContent.series, series)
+            .innerJoin(seriesContent.content, audioContent)
+            .where(where)
+            .fetch()
+            .size
+
+        return itemCount > 0
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt
index 00c9866..82a63f4 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt
@@ -25,6 +25,10 @@ enum class SeriesState {
     PROCEEDING, SUSPEND, COMPLETE
 }
 
+enum class SeriesSortType {
+    NEWEST, POPULAR
+}
+
 @Entity
 data class Series(
     var title: String,