diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt index 5b146ec..7879e45 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt @@ -83,7 +83,7 @@ class AdminAudioContentQueryRepositoryImpl( .where(where) .offset(offset) .limit(limit) - .orderBy(audioContent.id.desc()) + .orderBy(audioContent.releaseDate.desc()) .fetch() } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt index b8f8873..d86afd3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -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)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 2966970..d083cad 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -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 fun findByCreatorId( creatorId: Long, + coverImageHost: String, isAdult: Boolean = false, sortType: SortType = SortType.NEWEST, offset: Long = 0, limit: Long = 10 - ): List + ): List 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 { + ): List { 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) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 09a65f1..31b0081 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -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 { 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 + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt index ed377b6..0049565 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt @@ -29,6 +29,8 @@ data class GetAudioContentDetailResponse( val likeCount: Int, val commentList: List, val commentCount: Int, + val isPin: Boolean, + val isAvailablePin: Boolean, val creator: AudioContentCreator ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt index 2986176..b4f304d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt @@ -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 ) -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 ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/pin/PinContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/pin/PinContent.kt new file mode 100644 index 0000000..2d6fcc3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/pin/PinContent.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/pin/PinContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/pin/PinContentRepository.kt new file mode 100644 index 0000000..1819821 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/pin/PinContentRepository.kt @@ -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, PinContentQueryRepository + +interface PinContentQueryRepository { + fun getPinContentList(memberId: Long, active: Boolean? = null): List + + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index 3cf3390..33ca0eb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -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 // 공지사항