콘텐츠 상단 고정 기능 추가 #120

Merged
klaus merged 12 commits from test into main 2024-01-29 02:45:42 +00:00
9 changed files with 190 additions and 24 deletions

View File

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

View File

@ -196,4 +196,26 @@ class AudioContentController(private val service: AudioContentService) {
fun releaseContent() = run {
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.QAudioContentCuration.audioContentCuration
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.event.QEvent.event
import kr.co.vividnext.sodalive.member.MemberRole
@ -33,11 +34,12 @@ interface AudioContentQueryRepository {
fun findBundleByContentId(contentId: Long): List<AudioContent>
fun findByCreatorId(
creatorId: Long,
coverImageHost: String,
isAdult: Boolean = false,
sortType: SortType = SortType.NEWEST,
offset: Long = 0,
limit: Long = 10
): List<AudioContent>
): List<GetAudioContentListItem>
fun findTotalCountByCreatorId(creatorId: Long, isAdult: Boolean = false): Int
fun getCreatorOtherContentList(
@ -139,11 +141,12 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
override fun findByCreatorId(
creatorId: Long,
coverImageHost: String,
isAdult: Boolean,
sortType: SortType,
offset: Long,
limit: Long
): List<AudioContent> {
): List<GetAudioContentListItem> {
val orderBy = when (sortType) {
SortType.NEWEST -> audioContent.releaseDate.desc()
SortType.PRICE_HIGH -> audioContent.price.desc()
@ -161,11 +164,28 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
}
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)
.offset(offset)
.limit(limit)
.orderBy(orderBy)
.orderBy(pinContent.isActive.desc(), pinContent.updatedAt.desc(), orderBy)
.fetch()
}
@ -594,6 +614,7 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
val where = audioContent.isActive.isFalse
.and(audioContent.releaseDate.isNotNull)
.and(audioContent.releaseDate.loe(LocalDateTime.now()))
.and(audioContent.duration.isNotNull)
return queryFactory
.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.order.OrderRepository
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.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
@ -49,6 +51,7 @@ class AudioContentService(
private val playbackTrackingRepository: PlaybackTrackingRepository,
private val commentRepository: AudioContentCommentRepository,
private val audioContentLikeRepository: AudioContentLikeRepository,
private val pinContentRepository: PinContentRepository,
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
@ -551,6 +554,25 @@ class AudioContentService(
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(
contentId = audioContent.id!!,
title = audioContent.title,
@ -576,6 +598,8 @@ class AudioContentService(
likeCount = likeCount,
commentList = commentList,
commentCount = commentCount,
isPin = isPin,
isAvailablePin = isAvailablePin,
creator = AudioContentCreator(
creatorId = creatorId,
nickname = creator.nickname,
@ -603,6 +627,7 @@ class AudioContentService(
val audioContentList = repository.findByCreatorId(
creatorId = creatorId,
coverImageHost = coverImageHost,
isAdult = member.auth != null,
sortType = sortType,
offset = offset,
@ -612,23 +637,15 @@ class AudioContentService(
val items = audioContentList
.map {
val commentCount = commentRepository
.totalCountCommentByContentId(it.id!!)
.totalCountCommentByContentId(it.contentId)
val likeCount = audioContentLikeRepository
.totalCountAudioContentLike(it.id!!)
.totalCountAudioContentLike(it.contentId)
GetAudioContentListItem(
contentId = it.id!!,
coverImageUrl = "$coverImageHost/${it.coverImage!!}",
title = it.title,
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()
)
it.likeCount = likeCount
it.commentCount = commentCount
it
}
return GetAudioContentListResponse(
@ -696,4 +713,41 @@ class AudioContentService(
fun getContentRankingSortTypeList(): List<String> {
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 commentList: List<GetAudioContentCommentListItem>,
val commentCount: Int,
val isPin: Boolean,
val isAvailablePin: Boolean,
val creator: AudioContentCreator
)

View File

@ -1,19 +1,22 @@
package kr.co.vividnext.sodalive.content
import com.querydsl.core.annotations.QueryProjection
data class GetAudioContentListResponse(
val totalCount: Int,
val items: List<GetAudioContentListItem>
)
data class GetAudioContentListItem(
data class GetAudioContentListItem @QueryProjection constructor(
val contentId: Long,
val coverImageUrl: String,
val title: String,
val price: Int,
val themeStr: String,
val duration: String?,
val likeCount: Int,
val commentCount: Int,
var likeCount: Int = 0,
var commentCount: Int = 0,
val isPin: Boolean,
val isAdult: 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,
userMember = member,
timezone = timezone,
limit = 4
limit = 3
)
// 오디오 콘텐츠
@ -224,7 +224,7 @@ class ExplorerService(
sortType = SortType.NEWEST,
member = member,
offset = 0,
limit = 4
limit = 3
).items
// 공지사항