탐색 메인 - API 추가

This commit is contained in:
Klaus 2023-08-01 10:23:49 +09:00
parent df861bf8a1
commit 049e1c41de
11 changed files with 376 additions and 1 deletions

View File

@ -0,0 +1,32 @@
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.member.Member
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/explorer")
class ExplorerController(private val service: ExplorerService) {
@GetMapping
fun getExplorer(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getExplorer(member))
}
@GetMapping("/search/channel")
fun getSearchChannel(
@RequestParam channel: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getSearchChannel(channel, member))
}
}

View File

@ -1,13 +1,22 @@
package kr.co.vividnext.sodalive.explorer
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.explorer.section.ExplorerSection
import kr.co.vividnext.sodalive.explorer.section.QExplorerSection.explorerSection
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
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.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.LocalDateTime
@Repository
class ExplorerQueryRepository(
@ -68,4 +77,68 @@ class ExplorerQueryRepository(
)
}
}
fun getSubscriberGrowthRankingCreators(limit: Long): List<Member> {
return queryFactory
.selectFrom(member)
.join(member.follower, creatorFollowing)
.where(
member.role.eq(MemberRole.CREATOR)
.and(creatorFollowing.createdAt.goe(LocalDateTime.now().minusMonths(1)))
.and(creatorFollowing.isActive.isTrue)
)
.groupBy(member.id)
.orderBy(member.follower.size().desc())
.limit(limit)
.fetch()
}
fun getNewCreators(): List<Member> {
return queryFactory
.selectFrom(member)
.where(
member.role.eq(MemberRole.CREATOR)
.and(member.createdAt.goe(LocalDateTime.now().minusDays(30)))
)
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.limit(20)
.fetch()
}
fun getExplorerSectionData(isAdult: Boolean): List<ExplorerSection> {
var explorerSectionCondition = explorerSection.isActive.eq(true)
if (!isAdult) {
explorerSectionCondition = explorerSectionCondition.and(explorerSection.isAdult.isFalse)
}
return queryFactory
.selectFrom(explorerSection)
.where(explorerSectionCondition)
.orderBy(explorerSection.orders.asc())
.fetch()
}
fun findMemberByTag(tags: List<String>): List<Member> {
return queryFactory
.selectFrom(member)
.leftJoin(member.tags, memberCreatorTag)
.join(memberCreatorTag.tag, creatorTag)
.where(
member.role.eq(MemberRole.CREATOR)
.and(creatorTag.tag.`in`(tags))
)
.fetch()
.distinct()
}
fun getSearchChannel(channel: String, accountId: Long): List<Member> {
return queryFactory.selectFrom(member)
.where(
member.nickname.containsIgnoreCase(channel)
.and(member.isActive.isTrue)
.and(member.id.ne(accountId))
.and(member.role.eq(MemberRole.CREATOR))
)
.fetch()
}
}

View File

@ -0,0 +1,145 @@
package kr.co.vividnext.sodalive.explorer
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberService
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional(readOnly = true)
class ExplorerService(
private val memberService: MemberService,
private val queryRepository: ExplorerQueryRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getExplorer(member: Member, growthRankingCreatorsLimit: Long = 20): GetExplorerResponse {
val sections = mutableListOf<GetExplorerSectionResponse>()
// 인기 급상승중 (subscriberGrowthRankingCreators)
val growthRankingCreators = queryRepository
.getSubscriberGrowthRankingCreators(limit = growthRankingCreatorsLimit)
.asSequence()
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
.map {
GetExplorerSectionCreatorResponse(
id = it.id!!,
nickname = it.nickname,
tags = it.tags
.asSequence()
.filter { tag -> tag.tag.isActive }
.map { tag -> tag.tag.tag }
.joinToString(" ") { tag -> "#$tag" },
profileImageUrl = if (it.profileImage != null) {
"$cloudFrontHost/${it.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
}
)
}
.toList()
val growthRankingSection = GetExplorerSectionResponse(
title = "인기 급상승중",
coloredTitle = "인기",
color = "FF5C49",
creators = growthRankingCreators
)
sections.add(growthRankingSection)
// 새로 시작 (newCreators)
val newCreators = queryRepository
.getNewCreators()
.asSequence()
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
.map {
GetExplorerSectionCreatorResponse(
id = it.id!!,
nickname = it.nickname,
tags = it.tags
.asSequence()
.filter { tag -> tag.tag.isActive }
.map { tag -> tag.tag.tag }
.joinToString(" ") { tag -> "#$tag" },
profileImageUrl = if (it.profileImage != null) {
"$cloudFrontHost/${it.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
}
)
}
.toList()
val newCreatorsSection = GetExplorerSectionResponse(
title = "새로 시작",
coloredTitle = "새로",
color = "5FD28F",
creators = newCreators
)
sections.add(newCreatorsSection)
// 관리자에서 설정한 타이틀과 크리에이터
sections.addAll(
queryRepository
.getExplorerSectionData(isAdult = member.auth != null)
.asSequence()
.map {
val tags = it.tags.asSequence().map { explorerSectionTag -> explorerSectionTag.tag!!.tag }.toList()
val creators = queryRepository.findMemberByTag(tags)
.asSequence()
.filter { creator ->
!memberService.isBlocked(
blockedMemberId = member.id!!,
memberId = creator.id!!
)
}
.toList()
GetExplorerSectionResponse(
it.title,
it.coloredTitle,
it.color,
creators = creators
.asSequence()
.map { account ->
GetExplorerSectionCreatorResponse(
id = account.id!!,
nickname = account.nickname,
tags = account.tags
.asSequence()
.filter { counselorTag -> counselorTag.tag.isActive }
.toList()
.joinToString(" ") { counselorTag ->
"#${counselorTag.tag.tag}"
},
profileImageUrl = if (account.profileImage != null) {
"$cloudFrontHost/${account.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
}
)
}.toList()
)
}
.toList()
)
return GetExplorerResponse(sections = sections)
}
fun getSearchChannel(channel: String, member: Member): List<GetRoomDetailUser> {
if (channel.length < 2) {
throw SodaException("두 글자 이상 입력 하셔야 합니다.")
}
return queryRepository.getSearchChannel(channel, member.id!!)
.asSequence()
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
.map { GetRoomDetailUser(it, cloudFrontHost) }
.toList()
}
}

View File

@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.explorer
data class GetExplorerResponse(val sections: List<GetExplorerSectionResponse>)
data class GetExplorerSectionResponse(
val title: String,
val coloredTitle: String?,
val color: String?,
val creators: List<GetExplorerSectionCreatorResponse>
)
data class GetExplorerSectionCreatorResponse(
val id: Long,
val nickname: String,
val tags: String,
val profileImageUrl: String
)

View File

@ -0,0 +1,43 @@
package kr.co.vividnext.sodalive.explorer.section
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.tag.CreatorTag
import javax.persistence.CascadeType
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 ExplorerSection(
var title: String,
var isAdult: Boolean,
@Column(nullable = false)
var orders: Int = 1
) : BaseEntity() {
@Column(nullable = true)
var coloredTitle: String? = null
@Column(nullable = true)
var color: String? = null
@Column(nullable = false)
var isActive: Boolean = true
@OneToMany(mappedBy = "explorerSection", cascade = [CascadeType.ALL], orphanRemoval = true)
var tags: MutableList<ExplorerSectionCreatorTag> = mutableListOf()
}
@Entity
class ExplorerSectionCreatorTag : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "explorer_section_id", nullable = false)
var explorerSection: ExplorerSection? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_tag_id", nullable = false)
var tag: CreatorTag? = null
}

View File

@ -696,7 +696,11 @@ class LiveRoomService(
websiteUrl = user.websiteUrl,
blogUrl = user.blogUrl,
introduce = user.introduce,
tags = "",
tags = user.tags
.asSequence()
.filter { it.tag.isActive }
.map { it.tag.tag }
.joinToString(" ") { tag -> "#$tag" },
isSpeaker = isSpeaker,
isManager = isManager,
isFollowing = isFollowing,

View File

@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive.member
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import kr.co.vividnext.sodalive.member.notification.MemberNotification
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
import javax.persistence.CascadeType
import javax.persistence.Column
import javax.persistence.Entity
@ -33,6 +35,12 @@ data class Member(
@OneToMany(mappedBy = "member", cascade = [CascadeType.ALL])
val stipulationAgrees: MutableList<StipulationAgree> = mutableListOf()
@OneToMany(mappedBy = "member", cascade = [CascadeType.ALL], orphanRemoval = true)
var tags: MutableList<MemberCreatorTag> = mutableListOf()
@OneToMany(mappedBy = "creator")
var follower: MutableList<CreatorFollowing> = mutableListOf()
@OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
var notification: MemberNotification? = null

View File

@ -332,4 +332,6 @@ class MemberService(
blockMember.isActive = true
}
}
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
}

View File

@ -10,6 +10,7 @@ interface BlockMemberRepository : JpaRepository<BlockMember, Long>, BlockMemberQ
interface BlockMemberQueryRepository {
fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember?
fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean
}
@Repository
@ -24,4 +25,18 @@ class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
.orderBy(blockMember.id.desc())
.fetchFirst()
}
override fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean {
val blockedAccount = queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.memberId.eq(memberId)
.and(blockMember.blockedMemberId.eq(blockedMemberId))
.and(blockMember.isActive.isTrue)
)
.fetchOne()
return blockedAccount != null
}
}

View File

@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.member.tag
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
@Entity
data class CreatorTag(
@Column(unique = true, nullable = false)
var tag: String,
@Column(nullable = true)
var image: String? = null,
@Column(nullable = false)
var isActive: Boolean = true,
@Column(nullable = false)
var orders: Int = 1
) : BaseEntity()

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.member.tag
import kr.co.vividnext.sodalive.common.BaseEntity
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 MemberCreatorTag(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_tag_id", nullable = false)
var tag: CreatorTag
) : BaseEntity()