관리자 큐레이션 아이템

- 조회, 추가, 삭제, 콘텐츠 검색, 시리즈 검색 API 추가
This commit is contained in:
Klaus 2025-01-31 21:58:31 +09:00
parent 155ea5c5e4
commit 705459ee90
11 changed files with 327 additions and 2 deletions

View File

@ -32,6 +32,7 @@ interface AdminAudioContentQueryRepository {
): List<GetAdminContentListItem> ): List<GetAdminContentListItem>
fun getHashTagList(audioContentId: Long): List<String> fun getHashTagList(audioContentId: Long): List<String>
fun findByIdAndActiveTrue(audioContentId: Long): AudioContent?
} }
class AdminAudioContentQueryRepositoryImpl( class AdminAudioContentQueryRepositoryImpl(
@ -143,6 +144,16 @@ class AdminAudioContentQueryRepositoryImpl(
.fetch() .fetch()
} }
override fun findByIdAndActiveTrue(audioContentId: Long): AudioContent? {
return queryFactory
.select(audioContent)
.where(
audioContent.id.eq(audioContentId),
audioContent.isActive.isTrue
)
.fetchFirst()
}
private fun formattedDateExpression( private fun formattedDateExpression(
dateTime: DateTimePath<LocalDateTime>, dateTime: DateTimePath<LocalDateTime>,
format: String = "%Y-%m-%d" format: String = "%Y-%m-%d"

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class AddItemToCurationRequest(
val curationId: Long,
val contentIdList: List<Long>
)

View File

@ -33,4 +33,31 @@ class AdminContentCurationController(private val service: AdminContentCurationSe
fun getContentCurationList( fun getContentCurationList(
@RequestParam tabId: Long @RequestParam tabId: Long
) = ApiResponse.ok(service.getContentCurationList(tabId = tabId)) ) = ApiResponse.ok(service.getContentCurationList(tabId = tabId))
@GetMapping("/items")
fun getCurationItems(
@RequestParam curationId: Long
) = ApiResponse.ok(service.getCurationItem(curationId = curationId))
@GetMapping("/search/content")
fun searchCurationContentItem(
@RequestParam curationId: Long,
@RequestParam searchWord: String
) = ApiResponse.ok(service.searchCurationContentItem(curationId, searchWord))
@GetMapping("/search/series")
fun searchCurationSeriesItem(
@RequestParam curationId: Long,
@RequestParam searchWord: String
) = ApiResponse.ok(service.searchCurationSeriesItem(curationId, searchWord))
@PostMapping("/add/item")
fun addItemToCuration(@RequestBody request: AddItemToCurationRequest) {
ApiResponse.ok(service.addItemToCuration(request), "큐레이션 아이템을 등록했습니다.")
}
@PostMapping("/remove/item")
fun removeItemInCuration(@RequestBody request: RemoveItemInCurationRequest) {
ApiResponse.ok(service.removeItemInCuration(request), "큐레이션 아이템을 제거했습니다.")
}
} }

View File

@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCurationItem.audioContentCurationItem
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
import org.springframework.data.jpa.repository.JpaRepository
interface AdminContentCurationItemRepository :
JpaRepository<AudioContentCurationItem, Long>,
AdminContentCurationItemQueryRepository
interface AdminContentCurationItemQueryRepository {
fun findByCurationIdAndSeriesId(curationId: Long, seriesId: Long?): AudioContentCurationItem?
fun findByCurationIdAndContentId(curationId: Long, contentId: Long?): AudioContentCurationItem?
}
class AdminContentCurationItemQueryRepositoryImpl(
val queryFactory: JPAQueryFactory
) : AdminContentCurationItemQueryRepository {
override fun findByCurationIdAndSeriesId(curationId: Long, seriesId: Long?): AudioContentCurationItem? {
return queryFactory
.selectFrom(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.series, series)
.where(
audioContentCurationItem.curation.id.eq(curationId),
audioContentCurationItem.series.id.eq(seriesId)
)
.fetchFirst()
}
override fun findByCurationIdAndContentId(curationId: Long, contentId: Long?): AudioContentCurationItem? {
return queryFactory
.selectFrom(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.content, audioContent)
.where(
audioContentCurationItem.curation.id.eq(curationId),
audioContentCurationItem.content.id.eq(contentId)
)
.fetchFirst()
}
}

View File

@ -1,9 +1,14 @@
package kr.co.vividnext.sodalive.admin.content.curation package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCurationItem.audioContentCurationItem
import kr.co.vividnext.sodalive.content.main.tab.QAudioContentMainTab.audioContentMainTab import kr.co.vividnext.sodalive.content.main.tab.QAudioContentMainTab.audioContentMainTab
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@ -15,11 +20,18 @@ interface AdminContentCurationRepository :
interface AdminContentCurationQueryRepository { interface AdminContentCurationQueryRepository {
fun getAudioContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> fun getAudioContentCurationList(tabId: Long): List<GetAdminContentCurationResponse>
fun findByIdAndActive(id: Long): AudioContentCuration? fun findByIdAndActive(id: Long): AudioContentCuration?
fun findByCurationIdAndItemId(curationId: Long, itemId: Long): AudioContentCurationItem?
fun getAudioContentCurationItemList(curationId: Long): List<GetCurationItemResponse>
fun searchCurationContentItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse>
fun searchCurationSeriesItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse>
} }
@Repository @Repository
class AdminContentCurationQueryRepositoryImpl( class AdminContentCurationQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) : AdminContentCurationQueryRepository { ) : AdminContentCurationQueryRepository {
override fun getAudioContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> { override fun getAudioContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> {
return queryFactory return queryFactory
@ -52,4 +64,89 @@ class AdminContentCurationQueryRepositoryImpl(
) )
.fetchFirst() .fetchFirst()
} }
override fun findByCurationIdAndItemId(curationId: Long, itemId: Long): AudioContentCurationItem? {
return queryFactory.selectFrom(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.where(audioContentCuration.id.eq(curationId), audioContentCurationItem.id.eq(itemId))
.fetchFirst()
}
override fun getAudioContentCurationItemList(curationId: Long): List<GetCurationItemResponse> {
return queryFactory
.select(
QGetCurationItemResponse(
audioContentCurationItem.id,
audioContent.title.coalesce(series.title),
audioContent.detail.coalesce(series.introduction),
audioContent.coverImage.coalesce(series.coverImage).prepend("/").prepend(imageHost),
audioContent.member.nickname.coalesce(series.member.nickname).coalesce(""),
audioContent.isAdult.coalesce(series.isAdult)
)
)
.from(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.leftJoin(audioContentCurationItem.series, series)
.leftJoin(audioContentCurationItem.content, audioContent)
.where(
audioContentCuration.id.eq(curationId),
audioContentCurationItem.isActive.isTrue
)
.fetch()
}
override fun searchCurationContentItem(
curationId: Long,
searchWord: String
): List<SearchCurationItemResponse> {
return queryFactory
.select(
QSearchCurationItemResponse(
audioContent.id,
audioContent.title,
audioContent.coverImage.prepend("/").prepend(imageHost)
)
)
.from(audioContent)
.leftJoin(audioContentCurationItem)
.on(
audioContent.id.eq(audioContentCurationItem.content.id)
.and(audioContentCurationItem.curation.id.eq(curationId))
)
.where(
audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue)
.and(audioContent.title.contains(searchWord))
.and(audioContentCurationItem.id.isNull)
)
.fetch()
}
override fun searchCurationSeriesItem(
curationId: Long,
searchWord: String
): List<SearchCurationItemResponse> {
return queryFactory
.select(
QSearchCurationItemResponse(
series.id,
series.title,
series.coverImage.prepend("/").prepend(imageHost)
)
)
.from(series)
.leftJoin(audioContentCurationItem)
.on(
series.id.eq(audioContentCurationItem.series.id)
.and(audioContentCurationItem.curation.id.eq(curationId))
)
.where(
series.isActive.isTrue
.and(series.member.isNotNull)
.and(series.title.contains(searchWord))
.and(audioContentCurationItem.id.isNull)
)
.fetch()
}
} }

View File

@ -1,8 +1,11 @@
package kr.co.vividnext.sodalive.admin.content.curation package kr.co.vividnext.sodalive.admin.content.curation
import kr.co.vividnext.sodalive.admin.content.AdminContentRepository
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -10,7 +13,10 @@ import org.springframework.transaction.annotation.Transactional
@Service @Service
class AdminContentCurationService( class AdminContentCurationService(
private val repository: AdminContentCurationRepository, private val repository: AdminContentCurationRepository,
private val contentMainTabRepository: AdminContentMainTabRepository private val contentMainTabRepository: AdminContentMainTabRepository,
private val seriesRepository: AdminContentSeriesRepository,
private val contentRepository: AdminContentRepository,
private val contentCurationItemRepository: AdminContentCurationItemRepository
) { ) {
@Transactional @Transactional
fun createContentCuration(request: CreateContentCurationRequest) { fun createContentCuration(request: CreateContentCurationRequest) {
@ -76,4 +82,65 @@ class AdminContentCurationService(
fun getContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> { fun getContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> {
return repository.getAudioContentCurationList(tabId = tabId) return repository.getAudioContentCurationList(tabId = tabId)
} }
fun getCurationItem(curationId: Long): List<GetCurationItemResponse> {
return repository.getAudioContentCurationItemList(curationId)
}
fun searchCurationContentItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse> {
return repository.searchCurationContentItem(curationId, searchWord)
}
fun searchCurationSeriesItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse> {
return repository.searchCurationSeriesItem(curationId, searchWord)
}
@Transactional
fun addItemToCuration(request: AddItemToCurationRequest) {
// 큐레이션 조회
val audioContentCuration = repository.findByIdOrNull(id = request.curationId)
?: throw SodaException("잘못된 요청입니다.")
if (audioContentCuration.isSeries) {
request.contentIdList.forEach { seriesId ->
val series = seriesRepository.findByIdAndActiveTrue(seriesId)
if (series != null) {
val item = contentCurationItemRepository.findByCurationIdAndSeriesId(
curationId = request.curationId,
seriesId = series.id
) ?: AudioContentCurationItem()
item.curation = audioContentCuration
item.series = series
item.isActive = true
contentCurationItemRepository.save(item)
}
}
} else {
request.contentIdList.forEach { contentId ->
val audioContent = contentRepository.findByIdAndActiveTrue(contentId)
if (audioContent != null) {
val item = contentCurationItemRepository.findByCurationIdAndContentId(
curationId = request.curationId,
contentId = audioContent.id
) ?: AudioContentCurationItem()
item.curation = audioContentCuration
item.content = audioContent
item.isActive = true
contentCurationItemRepository.save(item)
}
}
}
}
@Transactional
fun removeItemInCuration(request: RemoveItemInCurationRequest) {
val audioContentCurationItem = repository.findByCurationIdAndItemId(
curationId = request.curationId,
itemId = request.itemId
)
audioContentCurationItem?.isActive = false
}
} }

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.core.annotations.QueryProjection
data class GetCurationItemResponse @QueryProjection constructor(
val id: Long,
val title: String,
val desc: String,
val coverImageUrl: String,
val creatorNickname: String,
val isAdult: Boolean
)

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class RemoveItemInCurationRequest(
val curationId: Long,
val itemId: Long
)

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.core.annotations.QueryProjection
data class SearchCurationItemResponse @QueryProjection constructor(
val id: Long,
val title: String,
val coverImageUrl: String
)

View File

@ -22,6 +22,7 @@ interface AdminContentSeriesQueryRepository {
): List<GetAdminSeriesListItem> ): List<GetAdminSeriesListItem>
fun searchSeriesList(searchWord: String): List<GetAdminSearchSeriesListItem> fun searchSeriesList(searchWord: String): List<GetAdminSearchSeriesListItem>
fun findByIdAndActiveTrue(seriesId: Long): Series?
} }
class AdminContentSeriesQueryRepositoryImpl( class AdminContentSeriesQueryRepositoryImpl(
@ -97,4 +98,14 @@ class AdminContentSeriesQueryRepositoryImpl(
.orderBy(series.id.desc()) .orderBy(series.id.desc())
.fetch() .fetch()
} }
override fun findByIdAndActiveTrue(seriesId: Long): Series? {
return queryFactory
.selectFrom(series)
.where(
series.id.eq(seriesId),
series.isActive.isTrue
)
.fetchFirst()
}
} }

View File

@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.content.main.curation
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
import javax.persistence.Table
@Entity
@Table(name = "content_curation_item")
data class AudioContentCurationItem(
@Column(nullable = false)
var orders: Int = 1,
@Column(nullable = false)
var isActive: Boolean = true
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "curation_id", nullable = false)
var curation: AudioContentCuration? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "content_id", nullable = true)
var content: AudioContent? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id", nullable = true)
var series: Series? = null
}