Merge pull request '콘텐츠 상단 고정 기능 추가' (#120) from test into main

Reviewed-on: #120
This commit is contained in:
klaus 2024-01-29 02:45:41 +00:00
commit a91db4f956
9 changed files with 190 additions and 24 deletions

View File

@ -83,7 +83,7 @@ class AdminAudioContentQueryRepositoryImpl(
.where(where) .where(where)
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
.orderBy(audioContent.id.desc()) .orderBy(audioContent.releaseDate.desc())
.fetch() .fetch()
} }

View File

@ -196,4 +196,26 @@ class AudioContentController(private val service: AudioContentService) {
fun releaseContent() = run { fun releaseContent() = run {
ApiResponse.ok(service.releaseContent()) ApiResponse.ok(service.releaseContent())
} }
@PostMapping("/pin-to-the-top/{id}")
@PreAuthorize("hasRole('CREATOR')")
fun pinToTheTop(
@PathVariable id: Long,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.pinToTheTop(contentId = id, member = member))
}
@PutMapping("/unpin-at-the-top/{id}")
@PreAuthorize("hasRole('CREATOR')")
fun unpinAtTheTop(
@PathVariable id: Long,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.unpinAtTheTop(contentId = id, member = member))
}
} }

View File

@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioCon
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.QAudioContentCuration.audioContentCuration import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.order.QOrder.order import kr.co.vividnext.sodalive.content.order.QOrder.order
import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
import kr.co.vividnext.sodalive.event.QEvent.event import kr.co.vividnext.sodalive.event.QEvent.event
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@ -33,11 +34,12 @@ interface AudioContentQueryRepository {
fun findBundleByContentId(contentId: Long): List<AudioContent> fun findBundleByContentId(contentId: Long): List<AudioContent>
fun findByCreatorId( fun findByCreatorId(
creatorId: Long, creatorId: Long,
coverImageHost: String,
isAdult: Boolean = false, isAdult: Boolean = false,
sortType: SortType = SortType.NEWEST, sortType: SortType = SortType.NEWEST,
offset: Long = 0, offset: Long = 0,
limit: Long = 10 limit: Long = 10
): List<AudioContent> ): List<GetAudioContentListItem>
fun findTotalCountByCreatorId(creatorId: Long, isAdult: Boolean = false): Int fun findTotalCountByCreatorId(creatorId: Long, isAdult: Boolean = false): Int
fun getCreatorOtherContentList( fun getCreatorOtherContentList(
@ -139,11 +141,12 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
override fun findByCreatorId( override fun findByCreatorId(
creatorId: Long, creatorId: Long,
coverImageHost: String,
isAdult: Boolean, isAdult: Boolean,
sortType: SortType, sortType: SortType,
offset: Long, offset: Long,
limit: Long limit: Long
): List<AudioContent> { ): List<GetAudioContentListItem> {
val orderBy = when (sortType) { val orderBy = when (sortType) {
SortType.NEWEST -> audioContent.releaseDate.desc() SortType.NEWEST -> audioContent.releaseDate.desc()
SortType.PRICE_HIGH -> audioContent.price.desc() SortType.PRICE_HIGH -> audioContent.price.desc()
@ -161,11 +164,28 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
} }
return queryFactory return queryFactory
.selectFrom(audioContent) .select(
QGetAudioContentListItem(
audioContent.id,
audioContent.coverImage.prepend("/").prepend(coverImageHost),
audioContent.title,
audioContent.price,
audioContent.theme.theme,
audioContent.duration,
Expressions.constant(0),
Expressions.constant(0),
pinContent.id.isNotNull,
audioContent.isAdult,
audioContent.releaseDate.gt(LocalDateTime.now())
)
)
.from(audioContent)
.leftJoin(pinContent)
.on(audioContent.id.eq(pinContent.content.id).and(pinContent.isActive.ne(false)))
.where(where) .where(where)
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
.orderBy(orderBy) .orderBy(pinContent.isActive.desc(), pinContent.updatedAt.desc(), orderBy)
.fetch() .fetch()
} }
@ -594,6 +614,7 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
val where = audioContent.isActive.isFalse val where = audioContent.isActive.isFalse
.and(audioContent.releaseDate.isNotNull) .and(audioContent.releaseDate.isNotNull)
.and(audioContent.releaseDate.loe(LocalDateTime.now())) .and(audioContent.releaseDate.loe(LocalDateTime.now()))
.and(audioContent.duration.isNotNull)
return queryFactory return queryFactory
.select(audioContent.id) .select(audioContent.id)

View File

@ -16,6 +16,8 @@ import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeResponse
import kr.co.vividnext.sodalive.content.main.GetAudioContentRanking import kr.co.vividnext.sodalive.content.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.pin.PinContent
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
@ -49,6 +51,7 @@ class AudioContentService(
private val playbackTrackingRepository: PlaybackTrackingRepository, private val playbackTrackingRepository: PlaybackTrackingRepository,
private val commentRepository: AudioContentCommentRepository, private val commentRepository: AudioContentCommentRepository,
private val audioContentLikeRepository: AudioContentLikeRepository, private val audioContentLikeRepository: AudioContentLikeRepository,
private val pinContentRepository: PinContentRepository,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
@ -551,6 +554,25 @@ class AudioContentService(
null null
} }
val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = id,
memberId = member.id!!,
active = true
)
val isPin = if (member.id!! == audioContent.member!!.id!!) {
pinContent != null
} else {
false
}
val pinContentListCount = pinContentRepository.getPinContentList(memberId = member.id!!, active = true).size
val isAvailablePin = if (member.id!! == audioContent.member!!.id!!) {
pinContentListCount < 3
} else {
false
}
return GetAudioContentDetailResponse( return GetAudioContentDetailResponse(
contentId = audioContent.id!!, contentId = audioContent.id!!,
title = audioContent.title, title = audioContent.title,
@ -576,6 +598,8 @@ class AudioContentService(
likeCount = likeCount, likeCount = likeCount,
commentList = commentList, commentList = commentList,
commentCount = commentCount, commentCount = commentCount,
isPin = isPin,
isAvailablePin = isAvailablePin,
creator = AudioContentCreator( creator = AudioContentCreator(
creatorId = creatorId, creatorId = creatorId,
nickname = creator.nickname, nickname = creator.nickname,
@ -603,6 +627,7 @@ class AudioContentService(
val audioContentList = repository.findByCreatorId( val audioContentList = repository.findByCreatorId(
creatorId = creatorId, creatorId = creatorId,
coverImageHost = coverImageHost,
isAdult = member.auth != null, isAdult = member.auth != null,
sortType = sortType, sortType = sortType,
offset = offset, offset = offset,
@ -612,23 +637,15 @@ class AudioContentService(
val items = audioContentList val items = audioContentList
.map { .map {
val commentCount = commentRepository val commentCount = commentRepository
.totalCountCommentByContentId(it.id!!) .totalCountCommentByContentId(it.contentId)
val likeCount = audioContentLikeRepository val likeCount = audioContentLikeRepository
.totalCountAudioContentLike(it.id!!) .totalCountAudioContentLike(it.contentId)
GetAudioContentListItem( it.likeCount = likeCount
contentId = it.id!!, it.commentCount = commentCount
coverImageUrl = "$coverImageHost/${it.coverImage!!}",
title = it.title, it
price = it.price,
themeStr = it.theme!!.theme,
duration = it.duration,
likeCount = likeCount,
commentCount = commentCount,
isAdult = it.isAdult,
isScheduledToOpen = it.releaseDate != null && it.releaseDate!! > LocalDateTime.now()
)
} }
return GetAudioContentListResponse( return GetAudioContentListResponse(
@ -696,4 +713,41 @@ class AudioContentService(
fun getContentRankingSortTypeList(): List<String> { fun getContentRankingSortTypeList(): List<String> {
return listOf("매출", "댓글", "좋아요") return listOf("매출", "댓글", "좋아요")
} }
@Transactional
fun pinToTheTop(contentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
var pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
)
if (pinContent != null) {
pinContent.isActive = true
} else {
val pinContentList = pinContentRepository.getPinContentList(memberId = member.id!!)
pinContent = if (pinContentList.size >= 3) {
pinContentList[0]
} else {
PinContent()
}
pinContent.isActive = true
pinContent.member = member
pinContent.content = audioContent
pinContentRepository.save(pinContent)
}
}
@Transactional
fun unpinAtTheTop(contentId: Long, member: Member) {
val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
) ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
pinContent.isActive = false
}
} }

View File

@ -29,6 +29,8 @@ data class GetAudioContentDetailResponse(
val likeCount: Int, val likeCount: Int,
val commentList: List<GetAudioContentCommentListItem>, val commentList: List<GetAudioContentCommentListItem>,
val commentCount: Int, val commentCount: Int,
val isPin: Boolean,
val isAvailablePin: Boolean,
val creator: AudioContentCreator val creator: AudioContentCreator
) )

View File

@ -1,19 +1,22 @@
package kr.co.vividnext.sodalive.content package kr.co.vividnext.sodalive.content
import com.querydsl.core.annotations.QueryProjection
data class GetAudioContentListResponse( data class GetAudioContentListResponse(
val totalCount: Int, val totalCount: Int,
val items: List<GetAudioContentListItem> val items: List<GetAudioContentListItem>
) )
data class GetAudioContentListItem( data class GetAudioContentListItem @QueryProjection constructor(
val contentId: Long, val contentId: Long,
val coverImageUrl: String, val coverImageUrl: String,
val title: String, val title: String,
val price: Int, val price: Int,
val themeStr: String, val themeStr: String,
val duration: String?, val duration: String?,
val likeCount: Int, var likeCount: Int = 0,
val commentCount: Int, var commentCount: Int = 0,
val isPin: Boolean,
val isAdult: Boolean, val isAdult: Boolean,
val isScheduledToOpen: Boolean val isScheduledToOpen: Boolean
) )

View File

@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.content.pin
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.member.Member
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
@Entity
data class PinContent(var isActive: Boolean = true) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "content_id", nullable = false)
var content: AudioContent? = null
}

View File

@ -0,0 +1,44 @@
package kr.co.vividnext.sodalive.content.pin
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent
import org.springframework.data.jpa.repository.JpaRepository
interface PinContentRepository : JpaRepository<PinContent, Long>, PinContentQueryRepository
interface PinContentQueryRepository {
fun getPinContentList(memberId: Long, active: Boolean? = null): List<PinContent>
fun findByContentIdAndMemberId(contentId: Long, memberId: Long, active: Boolean? = null): PinContent?
}
class PinContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : PinContentQueryRepository {
override fun getPinContentList(memberId: Long, active: Boolean?): List<PinContent> {
var where = pinContent.member.id.eq(memberId)
if (active != null) {
where = where.and(pinContent.isActive.eq(active))
}
return queryFactory
.selectFrom(pinContent)
.where(where)
.orderBy(pinContent.isActive.asc(), pinContent.updatedAt.asc())
.fetch()
}
override fun findByContentIdAndMemberId(contentId: Long, memberId: Long, active: Boolean?): PinContent? {
var where = pinContent.content.id.eq(contentId)
.and(pinContent.member.id.eq(memberId))
if (active != null) {
where = where
.and(pinContent.isActive.eq(active))
}
return queryFactory
.selectFrom(pinContent)
.where(where)
.fetchFirst()
}
}

View File

@ -215,7 +215,7 @@ class ExplorerService(
creatorId, creatorId,
userMember = member, userMember = member,
timezone = timezone, timezone = timezone,
limit = 4 limit = 3
) )
// 오디오 콘텐츠 // 오디오 콘텐츠
@ -224,7 +224,7 @@ class ExplorerService(
sortType = SortType.NEWEST, sortType = SortType.NEWEST,
member = member, member = member,
offset = 0, offset = 0,
limit = 4 limit = 3
).items ).items
// 공지사항 // 공지사항