diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt new file mode 100644 index 0000000..c5b8f8b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.explorer + +data class CreatorResponse( + val creatorId: Long, + val profileUrl: String, + val nickname: String, + val tags: List, + val introduce: String = "", + val instagramUrl: String? = null, + val youtubeUrl: String? = null, + val websiteUrl: String? = null, + val blogUrl: String? = null, + val isNotification: Boolean, + val notificationRecipientCount: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt index 574e2ca..b5d9f7b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt @@ -2,9 +2,18 @@ package kr.co.vividnext.sodalive.explorer import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest +import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest +import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -29,4 +38,52 @@ class ExplorerController(private val service: ExplorerService) { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") ApiResponse.ok(service.getSearchChannel(channel, member)) } + + @GetMapping("/profile/{id}") + fun getCreatorProfile( + @PathVariable("id") creatorId: Long, + @RequestParam timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getCreatorProfile(creatorId, timezone, member)) + } + + @PostMapping("/profile/cheers") + fun writeCheers( + @RequestBody request: PostWriteCheersRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.writeCheers(request, member), "등록되었습니다.") + } + + @PutMapping("/profile/cheers") + fun modifyCheers( + @RequestBody request: PutWriteCheersRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.modifyCheers(request, member), "수정되었습니다.") + } + + @PostMapping("/profile/notice") + @PreAuthorize("hasAnyRole('CREATOR')") + fun postCreatorNotice( + @RequestBody request: PostCreatorNoticeRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.saveNotice(member, request.notice), "공지사항이 저장되었습니다.") + } + + @GetMapping("/profile/{id}/follower-list") + fun getFollowerList( + @PathVariable("id") creatorId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getFollowerList(creatorId, member, pageable)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index 6a13f67..eb29c23 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -1,22 +1,40 @@ package kr.co.vividnext.sodalive.explorer +import com.querydsl.core.types.Predicate +import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.QUseCan.useCan +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListDto +import kr.co.vividnext.sodalive.explorer.follower.QGetFollowerListDto +import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers +import kr.co.vividnext.sodalive.explorer.profile.QChannelNotice.channelNotice +import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers +import kr.co.vividnext.sodalive.explorer.profile.TimeDifferenceResult import kr.co.vividnext.sodalive.explorer.section.ExplorerSection import kr.co.vividnext.sodalive.explorer.section.QExplorerSection.explorerSection +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.live.room.cancel.QLiveRoomCancel.liveRoomCancel +import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.member.auth.QAuth.auth import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository +import java.time.Duration import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter @Repository class ExplorerQueryRepository( @@ -141,4 +159,442 @@ class ExplorerQueryRepository( ) .fetch() } + + fun getAccount(userId: Long): Member? { + return queryFactory + .selectFrom(member) + .where(member.id.eq(userId)) + .fetchFirst() + } + + fun getUserDonationRanking( + creatorId: Long, + limit: Long, + offset: Long = 0, + withDonationCan: Boolean + ): List { + val creatorMember = QMember("creator") + val userMember = QMember("user") + + val donation = useCan.rewardCan.add(useCan.can).sum() + return queryFactory + .select(userMember, donation) + .from(useCan) + .join(useCan.room, liveRoom) + .join(liveRoom.member, creatorMember) + .join(useCan.member, userMember) + .offset(offset) + .limit(limit) + .where( + useCan.canUsage.eq(CanUsage.DONATION) + .and(useCan.isRefund.isFalse) + .and(creatorMember.id.eq(creatorId)) + ) + .groupBy(useCan.member.id) + .orderBy(donation.desc(), userMember.id.desc()) + .fetch() + .map { + val account = it.get(userMember)!! + val donationCan = it.get(donation)!! + UserDonationRankingResponse( + account.id!!, + account.nickname, + if (account.profileImage != null) { + "$cloudFrontHost/${account.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + if (withDonationCan) donationCan else 0 + ) + } + } + + fun getSimilarCreatorList(creatorId: Long): List { + val creator = queryFactory + .selectFrom(member) + .where(member.id.eq(creatorId)) + .fetchFirst() ?: throw SodaException("없는 사용자 입니다.") + + val creatorTagCount = creator.tags + .asSequence() + .filter { it.tag.isActive } + .map { it.tag } + .toList().size + + if (creatorTagCount <= 0) { + val where = member.role.eq(MemberRole.CREATOR) + .and(member.id.ne(creatorId)) + .and(member.tags.size().gt(0)) + .and(memberCreatorTag.tag.isActive.isTrue) + + return getRandomCreatorList(where = where, limit = 3) + } + + val cnt = memberCreatorTag.member.count() + val similarCreators = queryFactory + .select(member, cnt) + .from(memberCreatorTag) + .innerJoin(memberCreatorTag.member, member) + .innerJoin(memberCreatorTag.tag, creatorTag) + .leftJoin(member.auth, auth) + .where( + member.role.eq(MemberRole.CREATOR) + .and(member.id.ne(creatorId)) + .and( + memberCreatorTag.tag.`in`( + creator.tags + .asSequence() + .filter { it.tag.isActive } + .map { it.tag } + .toList() + ) + ) + ) + .groupBy(memberCreatorTag.member.id) + .orderBy(cnt.desc()) + .limit(3) + .fetch() + .map { + val account = it.get(member)!! + SimilarCreatorResponse( + account.id!!, + account.nickname, + if (account.profileImage != null) { + "$cloudFrontHost/${account.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + account.tags + .asSequence() + .filter { accountCounselorTag -> accountCounselorTag.tag.isActive } + .map { accountCounselorTag -> accountCounselorTag.tag.tag } + .toList() + ) + } + + if (similarCreators.size < 3) { + val userIds = similarCreators.map { it.userId } + var where = member.role.eq(MemberRole.CREATOR) + .and(member.id.ne(creatorId)) + .and(member.tags.size().gt(0)) + .and(memberCreatorTag.tag.isActive.isTrue) + + for (userId in userIds) { + where = where.and(member.id.ne(userId)) + } + + val additionalCreator = getRandomCreatorList(where = where, limit = (3 - similarCreators.size).toLong()) + return similarCreators + additionalCreator + } else { + return similarCreators + } + } + + private fun getRandomCreatorList(where: Predicate, limit: Long): List { + val result = queryFactory + .select(member) + .from(memberCreatorTag) + .join(memberCreatorTag.member, member) + .innerJoin(memberCreatorTag.tag, creatorTag) + .leftJoin(member.auth, auth) + .groupBy(memberCreatorTag.member.id) + .where(where) + .limit(limit) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .fetch() + .map { + SimilarCreatorResponse( + it.id!!, + it.nickname, + if (it.profileImage != null) { + "$cloudFrontHost/${it.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + it.tags + .asSequence() + .filter { accountCounselorTag -> accountCounselorTag.tag.isActive } + .map { accountCounselorTag -> accountCounselorTag.tag.tag } + .toList() + ) + } + return result + } + + fun getLiveRoomList( + creatorId: Long, + userMember: Member, + timezone: String, + limit: Int, + offset: Long = 0 + ): List { + var where = liveRoom.member.id.eq(creatorId) + .and(liveRoom.cancel.id.isNull) + .and(liveRoom.isActive.isTrue) + + if (userMember.auth == null) { + where = where.and(liveRoom.isAdult.isFalse) + } + + val result = mutableListOf() + + if (offset == 0L) { + result.addAll( + queryFactory + .selectFrom(liveRoom) + .innerJoin(liveRoom.member, member) + .leftJoin(liveRoom.cancel, liveRoomCancel) + .where(where) + .orderBy(liveRoom.beginDateTime.asc()) + .fetch() + ) + } + + return result + .map { + val reservations = it.reservations + .filter { reservation -> + reservation.member!!.id!! == userMember.id!! && reservation.isActive + } + + val beginDateTime = it.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + val isPaid = if (it.channelName != null) { + val useCan = queryFactory + .selectFrom(useCan) + .where( + useCan.member.id.eq(member.id) + .and(useCan.room.id.eq(it.id!!)) + .and(useCan.canUsage.eq(CanUsage.LIVE)) + ) + .orderBy(useCan.id.desc()) + .fetchFirst() + + useCan != null + } else { + false + } + + LiveRoomResponse( + roomId = it.id!!, + title = it.title, + content = it.notice, + isPaid = isPaid, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + isAdult = it.isAdult, + price = it.price, + channelName = it.channelName, + managerNickname = it.member!!.nickname, + coverImageUrl = if (it.coverImage!!.startsWith("https://")) { + it.coverImage!! + } else { + "$cloudFrontHost/${it.coverImage!!}" + }, + isReservation = reservations.isNotEmpty(), + isActive = it.isActive, + isPrivateRoom = it.type == LiveRoomType.PRIVATE + ) + } + } + + fun getNoticeString(creatorId: Long): String { + return queryFactory + .select(channelNotice.notice) + .from(channelNotice) + .where(channelNotice.member.id.eq(creatorId)) + .fetchFirst() ?: "" + } + + fun getCheersList(creatorId: Long, timezone: String, offset: Long, limit: Long): GetCheersResponse { + val totalCount = queryFactory + .selectFrom(creatorCheers) + .where( + creatorCheers.creator.id.eq(creatorId) + .and(creatorCheers.isActive.isTrue) + .and(creatorCheers.parent.isNull) + ) + .fetch() + .count() + + val cheers = queryFactory + .selectFrom(creatorCheers) + .where( + creatorCheers.creator.id.eq(creatorId) + .and(creatorCheers.isActive.isTrue) + .and(creatorCheers.parent.isNull) + ) + .offset(offset) + .limit(limit) + .orderBy(creatorCheers.id.desc()) + .fetch() + .asSequence() + .map { + val date = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetCheersResponseItem( + cheersId = it.id!!, + nickname = it.member!!.nickname, + profileUrl = if (it.member!!.profileImage != null) { + "$cloudFrontHost/${it.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + content = it.cheers, + date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + replyList = it.children.asSequence() + .map { cheers -> + val replyDate = cheers.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetCheersResponseItem( + cheersId = cheers.id!!, + nickname = cheers.member!!.nickname, + profileUrl = if (cheers.member!!.profileImage != null) { + "$cloudFrontHost/${cheers.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + content = cheers.cheers, + date = replyDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + replyList = listOf() + ) + } + .toList() + ) + } + .toList() + + return GetCheersResponse( + totalCount = totalCount, + cheers = cheers + ) + } + + fun getLiveCount(creatorId: Long): Long? { + return queryFactory + .select(liveRoom.id.count()) + .from(liveRoom) + .where( + liveRoom.member.id.eq(creatorId) + .and(liveRoom.channelName.isNotNull) + ) + .fetchFirst() + } + + fun getLiveTime(creatorId: Long): Long { + val diffs = queryFactory + .select( + Projections.constructor( + TimeDifferenceResult::class.java, + liveRoom.beginDateTime, + liveRoom.updatedAt + ) + ) + .from(liveRoom) + .where( + liveRoom.member.id.eq(creatorId) + .and(liveRoom.channelName.isNotNull) + ) + .fetch() + + return diffs + .asSequence() + .map { Duration.between(it.beginDateTime, it.updatedAt).toSeconds() } + .sum() / 3600 + } + + fun getLiveContributorCount(creatorId: Long): Long? { + return queryFactory + .select(liveRoomVisit.member.count()) + .from(liveRoomVisit) + .innerJoin(liveRoomVisit.room, liveRoom) + .where( + liveRoom.member.id.eq(creatorId) + .and(liveRoom.channelName.isNotNull) + ) + .fetchFirst() + } + + fun getFollowerListTotalCount(creatorId: Long): Int { + return queryFactory.select(creatorFollowing.id) + .from(creatorFollowing) + .innerJoin(creatorFollowing.member, member) + .where( + member.isActive.isTrue + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.creator.id.eq(creatorId)) + .and(creatorFollowing.member.id.ne(creatorId)) + ) + .fetch() + .size + } + + fun getFollowerList( + creatorId: Long, + offset: Long, + limit: Long + ): List { + return queryFactory + .select( + QGetFollowerListDto( + member.id, + member.profileImage.prepend("/").prepend(cloudFrontHost), + member.nickname, + member.role + ) + ) + .from(creatorFollowing) + .innerJoin(creatorFollowing.member, member) + .where( + member.isActive.isTrue + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.creator.id.eq(creatorId)) + .and(creatorFollowing.member.id.ne(creatorId)) + ) + .offset(offset) + .limit(limit) + .fetch() + } + + fun isFollow(creatorId: Long, memberId: Long): Boolean { + return queryFactory + .select(creatorFollowing.isActive) + .from(creatorFollowing) + .where( + creatorFollowing.creator.id.eq(creatorId) + .and(creatorFollowing.member.id.eq(memberId)) + ) + .fetchOne() ?: false + } + + fun getCreatorCheers(cheersId: Long): CreatorCheers? { + return queryFactory + .selectFrom(creatorCheers) + .where(creatorCheers.id.eq(cheersId)) + .fetchFirst() + } + + fun getCheers(cheersId: Long, memberId: Long): CreatorCheers? { + return queryFactory + .selectFrom(creatorCheers) + .where( + creatorCheers.id.eq(cheersId) + .and(creatorCheers.member.id.eq(memberId)) + ) + .fetchFirst() + } + + fun getNotice(creatorId: Long): ChannelNotice? { + return queryFactory + .selectFrom(channelNotice) + .where(channelNotice.member.id.eq(creatorId)) + .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 b0802c8..a517775 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -1,10 +1,20 @@ package kr.co.vividnext.sodalive.explorer import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse +import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponseItem +import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice +import kr.co.vividnext.sodalive.explorer.profile.ChannelNoticeRepository +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository +import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest +import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberService import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -13,6 +23,8 @@ import org.springframework.transaction.annotation.Transactional class ExplorerService( private val memberService: MemberService, private val queryRepository: ExplorerQueryRepository, + private val cheersRepository: CreatorCheersRepository, + private val noticeRepository: ChannelNoticeRepository, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String @@ -142,4 +154,156 @@ class ExplorerService( .map { GetRoomDetailUser(it, cloudFrontHost) } .toList() } + + fun getCreatorProfile(creatorId: Long, timezone: String, member: Member): GetCreatorProfileResponse { + // 크리에이터(유저) 정보 + val creatorAccount = queryRepository.getAccount(creatorId) ?: throw SodaException("없는 사용자 입니다.") + + // 차단된 사용자 체크 + val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = creatorId) + if (isBlocked) throw SodaException("${creatorAccount.nickname}님의 요청으로 채널 접근이 제한됩니다.") + + val notificationUserIds = queryRepository.getNotificationUserIds(creatorId) + val isNotification = notificationUserIds.contains(member.id) + val notificationRecipientCount = notificationUserIds.size + + // 후원랭킹 + val userDonationRanking = queryRepository.getUserDonationRanking( + creatorId, + 10, + withDonationCan = creatorId == member.id!! + ) + + // 추천 크리에이터 + val similarCreatorList = queryRepository.getSimilarCreatorList(creatorId) + + // 라이브 + val liveRoomList = queryRepository.getLiveRoomList( + creatorId, + userMember = member, + timezone = timezone, + limit = 4 + ) + + // 공지사항 + val notice = queryRepository.getNoticeString(creatorId) + + // 응원 + val cheers = queryRepository.getCheersList(creatorId, timezone = timezone, offset = 0, limit = 4) + + // 차단한 크리에이터 인지 체크 + val isBlock = memberService.isBlocked(blockedMemberId = creatorId, memberId = member.id!!) + + // 활동요약 (라이브 횟수, 라이브 시간, 라이브 참여자, 콘텐츠 수) + val liveCount = queryRepository.getLiveCount(creatorId) ?: 0 + val liveTime = queryRepository.getLiveTime(creatorId) + val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0 + val contentCount = 0L + + return GetCreatorProfileResponse( + creator = CreatorResponse( + creatorId = creatorAccount.id!!, + profileUrl = if (creatorAccount.profileImage != null) { + "$cloudFrontHost/${creatorAccount.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + nickname = creatorAccount.nickname, + tags = creatorAccount.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(), + introduce = creatorAccount.introduce, + instagramUrl = creatorAccount.instagramUrl, + youtubeUrl = creatorAccount.youtubeUrl, + websiteUrl = creatorAccount.websiteUrl, + blogUrl = creatorAccount.blogUrl, + isNotification = isNotification, + notificationRecipientCount = notificationRecipientCount + ), + userDonationRanking = userDonationRanking, + similarCreatorList = similarCreatorList, + liveRoomList = liveRoomList, + notice = notice, + cheers = cheers, + activitySummary = GetCreatorActivitySummary( + liveCount = liveCount, + liveTime = liveTime, + liveContributorCount = liveContributorCount, + contentCount = contentCount + ), + isBlock = isBlock + ) + } + + fun getFollowerList( + creatorId: Long, + member: Member, + pageable: Pageable + ): GetFollowerListResponse { + val totalCount = queryRepository + .getFollowerListTotalCount(creatorId) + + val followerList = queryRepository.getFollowerList(creatorId, pageable.offset, pageable.pageSize.toLong()) + .asSequence() + .map { + val isFollow = if (it.role == MemberRole.CREATOR) { + queryRepository.isFollow(creatorId = it.userId, memberId = member.id!!) + } else { + null + } + + GetFollowerListResponseItem( + userId = it.userId, + profileImage = it.profileImage, + nickname = it.nickname, + isFollow = isFollow + ) + } + .toList() + + return GetFollowerListResponse(totalCount = totalCount, items = followerList) + } + + @Transactional + fun writeCheers(request: PostWriteCheersRequest, member: Member) { + val creator = queryRepository.getAccount(request.creatorId) ?: throw SodaException("없는 사용자 입니다.") + + val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = request.creatorId) + if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 팬토크 작성이 제한됩니다.") + + val cheers = CreatorCheers(cheers = request.content) + cheers.member = member + cheers.creator = creator + + val parent = if (request.parentId != null) { + queryRepository.getCreatorCheers(request.parentId) + } else { + null + } + + if (parent != null) { + cheers.parent = parent + } + + cheersRepository.save(cheers) + } + + @Transactional + fun modifyCheers(request: PutWriteCheersRequest, member: Member) { + val cheers = queryRepository.getCheers(request.cheersId, member.id!!) + ?: throw SodaException("잘못된 요청입니다.") + + cheers.cheers = request.content + } + + @Transactional + fun saveNotice(member: Member, notice: String) { + var channelNotice = queryRepository.getNotice(creatorId = member.id!!) + + if (channelNotice == null) { + channelNotice = ChannelNotice(notice) + channelNotice.member = member + noticeRepository.save(channelNotice) + } else { + channelNotice.notice = notice + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt new file mode 100644 index 0000000..142f1b2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetCheersResponse( + val totalCount: Int, + val cheers: List +) + +data class GetCheersResponseItem( + val cheersId: Long, + val nickname: String, + val profileUrl: String, + val content: String, + val date: String, + val replyList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt new file mode 100644 index 0000000..3a1b5f6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetCreatorProfileResponse( + val creator: CreatorResponse, + val userDonationRanking: List, + val similarCreatorList: List, + val liveRoomList: List, + val notice: String, + val cheers: GetCheersResponse, + val activitySummary: GetCreatorActivitySummary, + val isBlock: Boolean +) + +data class GetCreatorActivitySummary( + val liveCount: Long, + val liveTime: Long, + val liveContributorCount: Long, + val contentCount: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt new file mode 100644 index 0000000..d21bd6b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetLiveRoomAllResponse( + val totalCount: Int, + val liveRoomList: List +) + +data class LiveRoomResponse( + val roomId: Long, + val title: String, + val content: String, + val isPaid: Boolean, + val beginDateTime: String, + val coverImageUrl: String, + val isAdult: Boolean, + val price: Int, + val channelName: String?, + val managerNickname: String, + val isReservation: Boolean, + val isActive: Boolean, + val isPrivateRoom: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt new file mode 100644 index 0000000..710baf7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer + +data class SimilarCreatorResponse( + val userId: Long, + val nickname: String, + val profileImage: String, + val tags: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetDonationAllResponse( + val accumulatedCansToday: Int, + val accumulatedCansLastWeek: Int, + val accumulatedCansThisMonth: Int, + val totalCount: Int, + val userDonationRanking: List +) + +data class UserDonationRankingResponse( + val userId: Long, + val nickname: String, + val profileImage: String, + val donationCan: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt new file mode 100644 index 0000000..9f16c0e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.explorer.follower + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.member.MemberRole + +data class GetFollowerListDto @QueryProjection constructor( + val userId: Long, + val profileImage: String, + val nickname: String, + val role: MemberRole +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt new file mode 100644 index 0000000..d7ef0ec --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.explorer.follower + +data class GetFollowerListResponse( + val totalCount: Int, + val items: List +) + +data class GetFollowerListResponseItem( + val userId: Long, + val profileImage: String, + val nickname: String, + val isFollow: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt new file mode 100644 index 0000000..33993b9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class ChannelNotice( + @Column(columnDefinition = "TEXT") + var notice: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt new file mode 100644 index 0000000..d506bb4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ChannelNoticeRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt new file mode 100644 index 0000000..56aee2e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt @@ -0,0 +1,36 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany + +@Entity +data class CreatorCheers( + @Column(columnDefinition = "TEXT", nullable = false) + var cheers: String, + val isActive: Boolean = true +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id", nullable = true) + var parent: CreatorCheers? = null + set(value) { + value?.children?.add(this) + field = value + } + + @OneToMany(mappedBy = "parent") + var children: MutableList = mutableListOf() + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id", nullable = false) + var creator: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt new file mode 100644 index 0000000..831264a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface CreatorCheersRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt new file mode 100644 index 0000000..c937b9b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.explorer.profile + +data class PostCreatorNoticeRequest(val notice: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt new file mode 100644 index 0000000..9d75c7f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.explorer.profile + +data class PostWriteCheersRequest( + val parentId: Long? = null, + val creatorId: Long, + val content: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt new file mode 100644 index 0000000..ebb7209 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.explorer.profile + +data class PutWriteCheersRequest( + val cheersId: Long, + val content: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt new file mode 100644 index 0000000..c1e62f9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import java.time.LocalDateTime + +data class TimeDifferenceResult( + val beginDateTime: LocalDateTime, + val updatedAt: LocalDateTime +)