콘텐츠 상단 고정 기능 추가 #120
| @@ -83,7 +83,7 @@ class AdminAudioContentQueryRepositoryImpl( | ||||
|             .where(where) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .orderBy(audioContent.id.desc()) | ||||
|             .orderBy(audioContent.releaseDate.desc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| ) | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
|         // 공지사항 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user