탐색 메인 - API 추가
This commit is contained in:
parent
df861bf8a1
commit
049e1c41de
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,22 @@
|
||||||
package kr.co.vividnext.sodalive.explorer
|
package kr.co.vividnext.sodalive.explorer
|
||||||
|
|
||||||
|
import com.querydsl.core.types.dsl.Expressions
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
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.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
|
||||||
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
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.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class ExplorerQueryRepository(
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -696,7 +696,11 @@ class LiveRoomService(
|
||||||
websiteUrl = user.websiteUrl,
|
websiteUrl = user.websiteUrl,
|
||||||
blogUrl = user.blogUrl,
|
blogUrl = user.blogUrl,
|
||||||
introduce = user.introduce,
|
introduce = user.introduce,
|
||||||
tags = "",
|
tags = user.tags
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.tag.isActive }
|
||||||
|
.map { it.tag.tag }
|
||||||
|
.joinToString(" ") { tag -> "#$tag" },
|
||||||
isSpeaker = isSpeaker,
|
isSpeaker = isSpeaker,
|
||||||
isManager = isManager,
|
isManager = isManager,
|
||||||
isFollowing = isFollowing,
|
isFollowing = isFollowing,
|
||||||
|
|
|
@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive.member
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
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.notification.MemberNotification
|
||||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
|
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
|
||||||
|
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
|
||||||
import javax.persistence.CascadeType
|
import javax.persistence.CascadeType
|
||||||
import javax.persistence.Column
|
import javax.persistence.Column
|
||||||
import javax.persistence.Entity
|
import javax.persistence.Entity
|
||||||
|
@ -33,6 +35,12 @@ data class Member(
|
||||||
@OneToMany(mappedBy = "member", cascade = [CascadeType.ALL])
|
@OneToMany(mappedBy = "member", cascade = [CascadeType.ALL])
|
||||||
val stipulationAgrees: MutableList<StipulationAgree> = mutableListOf()
|
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)
|
@OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
|
||||||
var notification: MemberNotification? = null
|
var notification: MemberNotification? = null
|
||||||
|
|
||||||
|
|
|
@ -332,4 +332,6 @@ class MemberService(
|
||||||
blockMember.isActive = true
|
blockMember.isActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface BlockMemberRepository : JpaRepository<BlockMember, Long>, BlockMemberQ
|
||||||
|
|
||||||
interface BlockMemberQueryRepository {
|
interface BlockMemberQueryRepository {
|
||||||
fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember?
|
fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember?
|
||||||
|
fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
|
@ -24,4 +25,18 @@ class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory)
|
||||||
.orderBy(blockMember.id.desc())
|
.orderBy(blockMember.id.desc())
|
||||||
.fetchFirst()
|
.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
@ -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()
|
Loading…
Reference in New Issue