Compare commits
11 Commits
7f3589dcfb
...
test
Author | SHA1 | Date | |
---|---|---|---|
59ca353b25 | |||
6bc65ec412 | |||
97e95b51ab | |||
a6dfa81ba6 | |||
dad517a953 | |||
eb2d093b02 | |||
67186bba55 | |||
edeecad2ce | |||
387f5388d9 | |||
adcaa0a5fd | |||
47b2c1cb93 |
@@ -39,7 +39,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(useCan.room, liveRoom)
|
.innerJoin(useCan.room, liveRoom)
|
||||||
.innerJoin(liveRoom.member, member)
|
.innerJoin(liveRoom.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.createdAt.goe(startDate))
|
.and(useCan.createdAt.goe(startDate))
|
||||||
@@ -75,7 +78,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(order.audioContent, audioContent)
|
.innerJoin(order.audioContent, audioContent)
|
||||||
.innerJoin(audioContent.member, member)
|
.innerJoin(audioContent.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
order.createdAt.goe(startDate)
|
order.createdAt.goe(startDate)
|
||||||
.and(order.createdAt.loe(endDate))
|
.and(order.createdAt.loe(endDate))
|
||||||
@@ -142,7 +148,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(order.audioContent, audioContent)
|
.innerJoin(order.audioContent, audioContent)
|
||||||
.innerJoin(audioContent.member, member)
|
.innerJoin(audioContent.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(order.isActive.isTrue)
|
.where(order.isActive.isTrue)
|
||||||
.groupBy(
|
.groupBy(
|
||||||
member.id,
|
member.id,
|
||||||
@@ -230,7 +239,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||||
.innerJoin(creatorCommunity.member, member)
|
.innerJoin(creatorCommunity.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||||
@@ -251,7 +263,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(useCan.room, liveRoom)
|
.innerJoin(useCan.room, liveRoom)
|
||||||
.innerJoin(liveRoom.member, member)
|
.innerJoin(liveRoom.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.createdAt.goe(startDate))
|
.and(useCan.createdAt.goe(startDate))
|
||||||
@@ -281,7 +296,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(useCan.room, liveRoom)
|
.innerJoin(useCan.room, liveRoom)
|
||||||
.innerJoin(liveRoom.member, member)
|
.innerJoin(liveRoom.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.createdAt.goe(startDate))
|
.and(useCan.createdAt.goe(startDate))
|
||||||
@@ -301,7 +319,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(order.audioContent, audioContent)
|
.innerJoin(order.audioContent, audioContent)
|
||||||
.innerJoin(audioContent.member, member)
|
.innerJoin(audioContent.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
order.createdAt.goe(startDate)
|
order.createdAt.goe(startDate)
|
||||||
.and(order.createdAt.loe(endDate))
|
.and(order.createdAt.loe(endDate))
|
||||||
@@ -331,7 +352,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(order.audioContent, audioContent)
|
.innerJoin(order.audioContent, audioContent)
|
||||||
.innerJoin(audioContent.member, member)
|
.innerJoin(audioContent.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
order.createdAt.goe(startDate)
|
order.createdAt.goe(startDate)
|
||||||
.and(order.createdAt.loe(endDate))
|
.and(order.createdAt.loe(endDate))
|
||||||
@@ -351,7 +375,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||||
.innerJoin(creatorCommunity.member, member)
|
.innerJoin(creatorCommunity.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||||
@@ -382,7 +409,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|||||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||||
.innerJoin(creatorCommunity.member, member)
|
.innerJoin(creatorCommunity.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||||
|
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.admin.calculate.ratio
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import java.time.LocalDateTime
|
||||||
import javax.persistence.Entity
|
import javax.persistence.Entity
|
||||||
import javax.persistence.FetchType
|
import javax.persistence.FetchType
|
||||||
import javax.persistence.JoinColumn
|
import javax.persistence.JoinColumn
|
||||||
@@ -9,12 +10,29 @@ import javax.persistence.OneToOne
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class CreatorSettlementRatio(
|
data class CreatorSettlementRatio(
|
||||||
val subsidy: Int,
|
var subsidy: Int,
|
||||||
val liveSettlementRatio: Int,
|
var liveSettlementRatio: Int,
|
||||||
val contentSettlementRatio: Int,
|
var contentSettlementRatio: Int,
|
||||||
val communitySettlementRatio: Int
|
var communitySettlementRatio: Int
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
@OneToOne(fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "member_id", nullable = false)
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
var member: Member? = null
|
var member: Member? = null
|
||||||
|
|
||||||
|
var deletedAt: LocalDateTime? = null
|
||||||
|
|
||||||
|
fun softDelete() {
|
||||||
|
this.deletedAt = LocalDateTime.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restore() {
|
||||||
|
this.deletedAt = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateValues(subsidy: Int, live: Int, content: Int, community: Int) {
|
||||||
|
this.subsidy = subsidy
|
||||||
|
this.liveSettlementRatio = live
|
||||||
|
this.contentSettlementRatio = content
|
||||||
|
this.communitySettlementRatio = community
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
|||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.security.access.prepost.PreAuthorize
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
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.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
@@ -27,4 +28,14 @@ class CreatorSettlementRatioController(private val service: CreatorSettlementRat
|
|||||||
limit = pageable.pageSize.toLong()
|
limit = pageable.pageSize.toLong()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@PostMapping("/update")
|
||||||
|
fun updateCreatorSettlementRatio(
|
||||||
|
@RequestBody request: CreateCreatorSettlementRatioRequest
|
||||||
|
) = ApiResponse.ok(service.updateCreatorSettlementRatio(request))
|
||||||
|
|
||||||
|
@PostMapping("/delete/{memberId}")
|
||||||
|
fun deleteCreatorSettlementRatio(
|
||||||
|
@PathVariable memberId: Long
|
||||||
|
) = ApiResponse.ok(service.deleteCreatorSettlementRatio(memberId))
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,9 @@ import org.springframework.data.jpa.repository.JpaRepository
|
|||||||
|
|
||||||
interface CreatorSettlementRatioRepository :
|
interface CreatorSettlementRatioRepository :
|
||||||
JpaRepository<CreatorSettlementRatio, Long>,
|
JpaRepository<CreatorSettlementRatio, Long>,
|
||||||
CreatorSettlementRatioQueryRepository
|
CreatorSettlementRatioQueryRepository {
|
||||||
|
fun findByMemberId(memberId: Long): CreatorSettlementRatio?
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatorSettlementRatioQueryRepository {
|
interface CreatorSettlementRatioQueryRepository {
|
||||||
fun getCreatorSettlementRatio(offset: Long, limit: Long): List<GetCreatorSettlementRatioItem>
|
fun getCreatorSettlementRatio(offset: Long, limit: Long): List<GetCreatorSettlementRatioItem>
|
||||||
@@ -21,6 +23,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
|
|||||||
return queryFactory
|
return queryFactory
|
||||||
.select(
|
.select(
|
||||||
QGetCreatorSettlementRatioItem(
|
QGetCreatorSettlementRatioItem(
|
||||||
|
member.id,
|
||||||
member.nickname,
|
member.nickname,
|
||||||
creatorSettlementRatio.subsidy,
|
creatorSettlementRatio.subsidy,
|
||||||
creatorSettlementRatio.liveSettlementRatio,
|
creatorSettlementRatio.liveSettlementRatio,
|
||||||
@@ -30,6 +33,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
|
|||||||
)
|
)
|
||||||
.from(creatorSettlementRatio)
|
.from(creatorSettlementRatio)
|
||||||
.innerJoin(creatorSettlementRatio.member, member)
|
.innerJoin(creatorSettlementRatio.member, member)
|
||||||
|
.where(creatorSettlementRatio.deletedAt.isNull)
|
||||||
.orderBy(creatorSettlementRatio.id.asc())
|
.orderBy(creatorSettlementRatio.id.asc())
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@@ -40,6 +44,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
|
|||||||
return queryFactory
|
return queryFactory
|
||||||
.select(creatorSettlementRatio.id)
|
.select(creatorSettlementRatio.id)
|
||||||
.from(creatorSettlementRatio)
|
.from(creatorSettlementRatio)
|
||||||
|
.where(creatorSettlementRatio.deletedAt.isNull)
|
||||||
.fetch()
|
.fetch()
|
||||||
.size
|
.size
|
||||||
}
|
}
|
||||||
|
@@ -14,8 +14,6 @@ class CreatorSettlementRatioService(
|
|||||||
) {
|
) {
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
|
fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
|
||||||
val creatorSettlementRatio = request.toEntity()
|
|
||||||
|
|
||||||
val creator = memberRepository.findByIdOrNull(request.memberId)
|
val creator = memberRepository.findByIdOrNull(request.memberId)
|
||||||
?: throw SodaException("잘못된 크리에이터 입니다.")
|
?: throw SodaException("잘못된 크리에이터 입니다.")
|
||||||
|
|
||||||
@@ -23,10 +21,52 @@ class CreatorSettlementRatioService(
|
|||||||
throw SodaException("잘못된 크리에이터 입니다.")
|
throw SodaException("잘못된 크리에이터 입니다.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val existing = repository.findByMemberId(request.memberId)
|
||||||
|
if (existing != null) {
|
||||||
|
// revive if soft-deleted, then update values
|
||||||
|
existing.restore()
|
||||||
|
existing.updateValues(
|
||||||
|
request.subsidy,
|
||||||
|
request.liveSettlementRatio,
|
||||||
|
request.contentSettlementRatio,
|
||||||
|
request.communitySettlementRatio
|
||||||
|
)
|
||||||
|
repository.save(existing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val creatorSettlementRatio = request.toEntity()
|
||||||
creatorSettlementRatio.member = creator
|
creatorSettlementRatio.member = creator
|
||||||
repository.save(creatorSettlementRatio)
|
repository.save(creatorSettlementRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun updateCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
|
||||||
|
val creator = memberRepository.findByIdOrNull(request.memberId)
|
||||||
|
?: throw SodaException("잘못된 크리에이터 입니다.")
|
||||||
|
if (creator.role != MemberRole.CREATOR) {
|
||||||
|
throw SodaException("잘못된 크리에이터 입니다.")
|
||||||
|
}
|
||||||
|
val existing = repository.findByMemberId(request.memberId)
|
||||||
|
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.")
|
||||||
|
existing.restore()
|
||||||
|
existing.updateValues(
|
||||||
|
request.subsidy,
|
||||||
|
request.liveSettlementRatio,
|
||||||
|
request.contentSettlementRatio,
|
||||||
|
request.communitySettlementRatio
|
||||||
|
)
|
||||||
|
repository.save(existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun deleteCreatorSettlementRatio(memberId: Long) {
|
||||||
|
val existing = repository.findByMemberId(memberId)
|
||||||
|
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.")
|
||||||
|
existing.softDelete()
|
||||||
|
repository.save(existing)
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getCreatorSettlementRatio(offset: Long, limit: Long): GetCreatorSettlementRatioResponse {
|
fun getCreatorSettlementRatio(offset: Long, limit: Long): GetCreatorSettlementRatioResponse {
|
||||||
val totalCount = repository.getCreatorSettlementRatioTotalCount()
|
val totalCount = repository.getCreatorSettlementRatioTotalCount()
|
||||||
|
@@ -8,6 +8,7 @@ data class GetCreatorSettlementRatioResponse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class GetCreatorSettlementRatioItem @QueryProjection constructor(
|
data class GetCreatorSettlementRatioItem @QueryProjection constructor(
|
||||||
|
val memberId: Long,
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
val subsidy: Int,
|
val subsidy: Int,
|
||||||
val liveSettlementRatio: Int,
|
val liveSettlementRatio: Int,
|
||||||
|
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.admin.chat.character
|
|||||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterRegisterRequest
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterRegisterRequest
|
||||||
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchListPageResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
|
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
|
||||||
@@ -73,14 +74,21 @@ class AdminChatCharacterController(
|
|||||||
/**
|
/**
|
||||||
* 캐릭터 검색(관리자)
|
* 캐릭터 검색(관리자)
|
||||||
* - 이름/설명/MBTI/태그 기준 부분 검색, 활성 캐릭터만 대상
|
* - 이름/설명/MBTI/태그 기준 부분 검색, 활성 캐릭터만 대상
|
||||||
* - 페이징 제거: 전체 목록 반환
|
* - 페이징 지원: page, size 파라미터 사용
|
||||||
*/
|
*/
|
||||||
@GetMapping("/search")
|
@GetMapping("/search")
|
||||||
fun searchCharacters(
|
fun searchCharacters(
|
||||||
@RequestParam("searchTerm") searchTerm: String
|
@RequestParam("searchTerm") searchTerm: String,
|
||||||
|
@RequestParam(defaultValue = "0") page: Int,
|
||||||
|
@RequestParam(defaultValue = "20") size: Int
|
||||||
) = run {
|
) = run {
|
||||||
val list = adminService.searchCharactersAll(searchTerm, imageHost)
|
val pageable = adminService.createDefaultPageRequest(page, size)
|
||||||
ApiResponse.ok(list)
|
val resultPage = adminService.searchCharacters(searchTerm, pageable, imageHost)
|
||||||
|
val response = ChatCharacterSearchListPageResponse(
|
||||||
|
totalCount = resultPage.totalElements,
|
||||||
|
content = resultPage.content
|
||||||
|
)
|
||||||
|
ApiResponse.ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.chat.character.dto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐릭터 검색 결과 페이지 응답 DTO
|
||||||
|
*/
|
||||||
|
data class ChatCharacterSearchListPageResponse(
|
||||||
|
val totalCount: Long,
|
||||||
|
val content: List<ChatCharacterListResponse>
|
||||||
|
)
|
@@ -3,16 +3,16 @@ package kr.co.vividnext.sodalive.admin.chat.character.dto
|
|||||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐릭터 검색 결과 응답 DTO
|
* 원작 연결된 캐릭터 결과 응답 DTO
|
||||||
*/
|
*/
|
||||||
data class ChatCharacterSearchResponse(
|
data class OriginalWorkChatCharacterResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val imagePath: String?
|
val imagePath: String?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(character: ChatCharacter, imageHost: String): ChatCharacterSearchResponse {
|
fun from(character: ChatCharacter, imageHost: String): OriginalWorkChatCharacterResponse {
|
||||||
return ChatCharacterSearchResponse(
|
return OriginalWorkChatCharacterResponse(
|
||||||
id = character.id!!,
|
id = character.id!!,
|
||||||
name = character.name,
|
name = character.name,
|
||||||
imagePath = character.imagePath?.let { "$imageHost/$it" }
|
imagePath = character.imagePath?.let { "$imageHost/$it" }
|
||||||
@@ -22,9 +22,9 @@ data class ChatCharacterSearchResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캐릭터 검색 결과 페이지 응답 DTO
|
* 원작 연결된 캐릭터 결과 페이지 응답 DTO
|
||||||
*/
|
*/
|
||||||
data class ChatCharacterSearchListPageResponse(
|
data class OriginalWorkChatCharacterListPageResponse(
|
||||||
val totalCount: Long,
|
val totalCount: Long,
|
||||||
val content: List<ChatCharacterSearchResponse>
|
val content: List<OriginalWorkChatCharacterResponse>
|
||||||
)
|
)
|
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.admin.chat.character.service
|
|||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterDetailResponse
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterDetailResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListPageResponse
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListPageResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListResponse
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchResponse
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
@@ -72,24 +71,8 @@ class AdminChatCharacterService(
|
|||||||
searchTerm: String,
|
searchTerm: String,
|
||||||
pageable: Pageable,
|
pageable: Pageable,
|
||||||
imageHost: String = ""
|
imageHost: String = ""
|
||||||
): Page<ChatCharacterSearchResponse> {
|
): Page<ChatCharacterListResponse> {
|
||||||
val characters = chatCharacterRepository.searchCharacters(searchTerm, pageable)
|
val characters = chatCharacterRepository.searchCharacters(searchTerm, pageable)
|
||||||
return characters.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
return characters.map { ChatCharacterListResponse.from(it, imageHost) }
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 캐릭터 검색 (이름, 설명, MBTI, 태그 기반) - 무페이징
|
|
||||||
*
|
|
||||||
* @param searchTerm 검색어
|
|
||||||
* @param imageHost 이미지 호스트 URL
|
|
||||||
* @return 검색된 캐릭터 목록 (전체)
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
fun searchCharactersAll(
|
|
||||||
searchTerm: String,
|
|
||||||
imageHost: String = ""
|
|
||||||
): List<ChatCharacterSearchResponse> {
|
|
||||||
val characters = chatCharacterRepository.searchCharactersNoPaging(searchTerm)
|
|
||||||
return characters.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,8 @@ package kr.co.vividnext.sodalive.admin.chat.original
|
|||||||
|
|
||||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchListPageResponse
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.OriginalWorkChatCharacterListPageResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchResponse
|
import kr.co.vividnext.sodalive.admin.chat.character.dto.OriginalWorkChatCharacterResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkAssignCharactersRequest
|
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkAssignCharactersRequest
|
||||||
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkPageResponse
|
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkPageResponse
|
||||||
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkRegisterRequest
|
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkRegisterRequest
|
||||||
@@ -172,9 +172,9 @@ class AdminOriginalWorkController(
|
|||||||
@RequestParam(defaultValue = "20") size: Int
|
@RequestParam(defaultValue = "20") size: Int
|
||||||
) = run {
|
) = run {
|
||||||
val pageRes = originalWorkService.getCharactersOfOriginalWorkPage(id, page, size)
|
val pageRes = originalWorkService.getCharactersOfOriginalWorkPage(id, page, size)
|
||||||
val content = pageRes.content.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
val content = pageRes.content.map { OriginalWorkChatCharacterResponse.from(it, imageHost) }
|
||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
ChatCharacterSearchListPageResponse(
|
OriginalWorkChatCharacterListPageResponse(
|
||||||
totalCount = pageRes.totalElements,
|
totalCount = pageRes.totalElements,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
@@ -12,7 +12,12 @@ data class OriginalWorkRegisterRequest(
|
|||||||
@JsonProperty("category") val category: String,
|
@JsonProperty("category") val category: String,
|
||||||
@JsonProperty("isAdult") val isAdult: Boolean = false,
|
@JsonProperty("isAdult") val isAdult: Boolean = false,
|
||||||
@JsonProperty("description") val description: String = "",
|
@JsonProperty("description") val description: String = "",
|
||||||
@JsonProperty("originalLink") val originalLink: String? = null
|
@JsonProperty("originalWork") val originalWork: String? = null,
|
||||||
|
@JsonProperty("originalLink") val originalLink: String? = null,
|
||||||
|
@JsonProperty("writer") val writer: String? = null,
|
||||||
|
@JsonProperty("studio") val studio: String? = null,
|
||||||
|
@JsonProperty("originalLinks") val originalLinks: List<String>? = null,
|
||||||
|
@JsonProperty("tags") val tags: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +30,12 @@ data class OriginalWorkUpdateRequest(
|
|||||||
@JsonProperty("category") val category: String? = null,
|
@JsonProperty("category") val category: String? = null,
|
||||||
@JsonProperty("isAdult") val isAdult: Boolean? = null,
|
@JsonProperty("isAdult") val isAdult: Boolean? = null,
|
||||||
@JsonProperty("description") val description: String? = null,
|
@JsonProperty("description") val description: String? = null,
|
||||||
@JsonProperty("originalLink") val originalLink: String? = null
|
@JsonProperty("originalWork") val originalWork: String? = null,
|
||||||
|
@JsonProperty("originalLink") val originalLink: String? = null,
|
||||||
|
@JsonProperty("writer") val writer: String? = null,
|
||||||
|
@JsonProperty("studio") val studio: String? = null,
|
||||||
|
@JsonProperty("originalLinks") val originalLinks: List<String>? = null,
|
||||||
|
@JsonProperty("tags") val tags: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +48,12 @@ data class OriginalWorkResponse(
|
|||||||
val category: String,
|
val category: String,
|
||||||
val isAdult: Boolean,
|
val isAdult: Boolean,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
val originalWork: String?,
|
||||||
val originalLink: String?,
|
val originalLink: String?,
|
||||||
|
val writer: String?,
|
||||||
|
val studio: String?,
|
||||||
|
val originalLinks: List<String>,
|
||||||
|
val tags: List<String>,
|
||||||
val imageUrl: String?
|
val imageUrl: String?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -55,7 +70,12 @@ data class OriginalWorkResponse(
|
|||||||
category = entity.category,
|
category = entity.category,
|
||||||
isAdult = entity.isAdult,
|
isAdult = entity.isAdult,
|
||||||
description = entity.description,
|
description = entity.description,
|
||||||
|
originalWork = entity.originalWork,
|
||||||
originalLink = entity.originalLink,
|
originalLink = entity.originalLink,
|
||||||
|
writer = entity.writer,
|
||||||
|
studio = entity.studio,
|
||||||
|
originalLinks = entity.originalLinks.map { it.url },
|
||||||
|
tags = entity.tagMappings.map { it.tag.tag },
|
||||||
imageUrl = fullImagePath
|
imageUrl = fullImagePath
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,9 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
|||||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||||
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
||||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTagMapping
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.repository.OriginalWorkTagRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
@@ -20,7 +23,8 @@ import org.springframework.transaction.annotation.Transactional
|
|||||||
@Service
|
@Service
|
||||||
class AdminOriginalWorkService(
|
class AdminOriginalWorkService(
|
||||||
private val originalWorkRepository: OriginalWorkRepository,
|
private val originalWorkRepository: OriginalWorkRepository,
|
||||||
private val chatCharacterRepository: ChatCharacterRepository
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
|
private val originalWorkTagRepository: OriginalWorkTagRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** 원작 등록 (중복 제목 방지 포함) */
|
/** 원작 등록 (중복 제목 방지 포함) */
|
||||||
@@ -35,8 +39,23 @@ class AdminOriginalWorkService(
|
|||||||
category = request.category,
|
category = request.category,
|
||||||
isAdult = request.isAdult,
|
isAdult = request.isAdult,
|
||||||
description = request.description,
|
description = request.description,
|
||||||
originalLink = request.originalLink
|
originalWork = request.originalWork,
|
||||||
|
originalLink = request.originalLink,
|
||||||
|
writer = request.writer,
|
||||||
|
studio = request.studio
|
||||||
)
|
)
|
||||||
|
// 링크 리스트 생성
|
||||||
|
request.originalLinks?.filter { it.isNotBlank() }?.forEach { link ->
|
||||||
|
entity.originalLinks.add(kr.co.vividnext.sodalive.chat.original.OriginalWorkLink(url = link, originalWork = entity))
|
||||||
|
}
|
||||||
|
// 태그 매핑 생성 (기존 태그 재사용)
|
||||||
|
request.tags?.let { tags ->
|
||||||
|
val normalized = tags.map { it.trim() }.filter { it.isNotBlank() }.toSet()
|
||||||
|
normalized.forEach { t ->
|
||||||
|
val tagEntity = originalWorkTagRepository.findByTag(t) ?: originalWorkTagRepository.save(OriginalWorkTag(t))
|
||||||
|
entity.tagMappings.add(OriginalWorkTagMapping(originalWork = entity, tag = tagEntity))
|
||||||
|
}
|
||||||
|
}
|
||||||
return originalWorkRepository.save(entity)
|
return originalWorkRepository.save(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +70,40 @@ class AdminOriginalWorkService(
|
|||||||
request.category?.let { ow.category = it }
|
request.category?.let { ow.category = it }
|
||||||
request.isAdult?.let { ow.isAdult = it }
|
request.isAdult?.let { ow.isAdult = it }
|
||||||
request.description?.let { ow.description = it }
|
request.description?.let { ow.description = it }
|
||||||
|
request.originalWork?.let { ow.originalWork = it }
|
||||||
request.originalLink?.let { ow.originalLink = it }
|
request.originalLink?.let { ow.originalLink = it }
|
||||||
|
request.writer?.let { ow.writer = it }
|
||||||
|
request.studio?.let { ow.studio = it }
|
||||||
|
// 링크 리스트가 전달되면 기존 것을 교체
|
||||||
|
request.originalLinks?.let { links ->
|
||||||
|
ow.originalLinks.clear()
|
||||||
|
links.filter { it.isNotBlank() }.forEach { link ->
|
||||||
|
ow.originalLinks.add(kr.co.vividnext.sodalive.chat.original.OriginalWorkLink(url = link, originalWork = ow))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 태그 변경사항만 반영 (요청이 null이면 변경 없음)
|
||||||
|
request.tags?.let { tags ->
|
||||||
|
val normalized = tags.map { it.trim() }.filter { it.isNotBlank() }.toSet()
|
||||||
|
val current = ow.tagMappings.map { it.tag.tag }.toSet()
|
||||||
|
val toAdd = normalized.minus(current)
|
||||||
|
val toRemove = current.minus(normalized)
|
||||||
|
|
||||||
|
if (toRemove.isNotEmpty()) {
|
||||||
|
val itr = ow.tagMappings.iterator()
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
val m = itr.next()
|
||||||
|
if (toRemove.contains(m.tag.tag)) {
|
||||||
|
itr.remove() // orphanRemoval=true로 매핑 삭제
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toAdd.isNotEmpty()) {
|
||||||
|
toAdd.forEach { t ->
|
||||||
|
val tagEntity = originalWorkTagRepository.findByTag(t) ?: originalWorkTagRepository.save(OriginalWorkTag(t))
|
||||||
|
ow.tagMappings.add(OriginalWorkTagMapping(originalWork = ow, tag = tagEntity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (imagePath != null) {
|
if (imagePath != null) {
|
||||||
ow.imagePath = imagePath
|
ow.imagePath = imagePath
|
||||||
}
|
}
|
||||||
@@ -145,11 +197,17 @@ class AdminOriginalWorkService(
|
|||||||
/** 단일 캐릭터를 지정 원작에 배정 */
|
/** 단일 캐릭터를 지정 원작에 배정 */
|
||||||
@Transactional
|
@Transactional
|
||||||
fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
|
fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
|
||||||
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
|
|
||||||
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
|
|
||||||
val character = chatCharacterRepository.findById(characterId)
|
val character = chatCharacterRepository.findById(characterId)
|
||||||
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") }
|
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") }
|
||||||
character.originalWork = ow
|
|
||||||
|
if (originalWorkId == 0L) {
|
||||||
|
character.originalWork = null
|
||||||
|
} else {
|
||||||
|
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
|
||||||
|
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
|
||||||
|
character.originalWork = ow
|
||||||
|
}
|
||||||
|
|
||||||
chatCharacterRepository.save(character)
|
chatCharacterRepository.save(character)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,6 @@ import org.springframework.stereotype.Repository
|
|||||||
interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
||||||
fun findByName(name: String): ChatCharacter?
|
fun findByName(name: String): ChatCharacter?
|
||||||
fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter>
|
fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter>
|
||||||
fun findByOriginalWorkIdAndIsActiveTrueOrderByCreatedAtDesc(originalWorkId: Long): List<ChatCharacter>
|
|
||||||
fun findByOriginalWorkIdAndIsActiveTrue(originalWorkId: Long, pageable: Pageable): Page<ChatCharacter>
|
fun findByOriginalWorkIdAndIsActiveTrue(originalWorkId: Long, pageable: Pageable): Page<ChatCharacter>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,28 +53,6 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
|||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): Page<ChatCharacter>
|
): Page<ChatCharacter>
|
||||||
|
|
||||||
/**
|
|
||||||
* 이름, 설명, MBTI, 태그로 캐릭터 검색 - 무페이징 전체 목록
|
|
||||||
*/
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
SELECT DISTINCT c FROM ChatCharacter c
|
|
||||||
LEFT JOIN c.tagMappings tm
|
|
||||||
LEFT JOIN tm.tag t
|
|
||||||
WHERE c.isActive = true AND
|
|
||||||
(
|
|
||||||
LOWER(c.name) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
|
||||||
LOWER(c.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
|
||||||
(c.mbti IS NOT NULL AND LOWER(c.mbti) LIKE LOWER(CONCAT('%', :searchTerm, '%'))) OR
|
|
||||||
(t.tag IS NOT NULL AND LOWER(t.tag) LIKE LOWER(CONCAT('%', :searchTerm, '%')))
|
|
||||||
)
|
|
||||||
ORDER BY c.createdAt DESC
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
fun searchCharactersNoPaging(
|
|
||||||
@Param("searchTerm") searchTerm: String
|
|
||||||
): List<ChatCharacter>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
|
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
|
||||||
*/
|
*/
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.original
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.CascadeType
|
||||||
import javax.persistence.Column
|
import javax.persistence.Column
|
||||||
import javax.persistence.Entity
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.OneToMany
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 원작(오리지널 작품) 엔티티
|
* 원작(오리지널 작품) 엔티티
|
||||||
@@ -31,13 +33,33 @@ class OriginalWork(
|
|||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
|
|
||||||
/** 원작 링크 */
|
/** 원천 원작 */
|
||||||
@Column(nullable = true)
|
@Column(nullable = true)
|
||||||
var originalLink: String? = null
|
var originalWork: String? = null,
|
||||||
|
|
||||||
|
/** 원천 원작 링크(단일) */
|
||||||
|
@Column(nullable = true)
|
||||||
|
var originalLink: String? = null,
|
||||||
|
|
||||||
|
/** 작가 */
|
||||||
|
@Column(nullable = true)
|
||||||
|
var writer: String? = null,
|
||||||
|
|
||||||
|
/** 제작사 */
|
||||||
|
@Column(nullable = true)
|
||||||
|
var studio: String? = null
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
/** 원작 대표 이미지 S3 경로 */
|
/** 원작 대표 이미지 S3 경로 */
|
||||||
var imagePath: String? = null
|
var imagePath: String? = null
|
||||||
|
|
||||||
/** 소프트 삭제 여부 (true면 삭제된 것으로 간주) */
|
/** 소프트 삭제 여부 (true면 삭제된 것으로 간주) */
|
||||||
var isDeleted: Boolean = false
|
var isDeleted: Boolean = false
|
||||||
|
|
||||||
|
/** 원작 링크들 (1:N) */
|
||||||
|
@OneToMany(mappedBy = "originalWork", cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||||
|
var originalLinks: MutableList<OriginalWorkLink> = mutableListOf()
|
||||||
|
|
||||||
|
/** 원작 태그 매핑들 (1:N) */
|
||||||
|
@OneToMany(mappedBy = "originalWork", cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||||
|
var tagMappings: MutableList<OriginalWorkTagMapping> = mutableListOf()
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원작 원본 링크 엔티티
|
||||||
|
* - 하나의 원작(OriginalWork)에 여러 개의 링크가 연결될 수 있음 (1:N)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
class OriginalWorkLink(
|
||||||
|
@Column(nullable = false)
|
||||||
|
var url: String,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "original_work_id")
|
||||||
|
var originalWork: OriginalWork? = null
|
||||||
|
) : BaseEntity()
|
@@ -33,13 +33,13 @@ interface OriginalWorkRepository : JpaRepository<OriginalWork, Long> {
|
|||||||
): List<OriginalWork>
|
): List<OriginalWork>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앱용 원작 목록 조회
|
* 앱용 원작 목록 조회 (페이징)
|
||||||
* - 소프트 삭제 제외
|
* - 소프트 삭제 제외
|
||||||
* - includeAdult=false이면 19금 제외
|
* - includeAdult=false이면 19금 제외
|
||||||
* - 활성 캐릭터가 하나라도 연결된 원작만 조회
|
* - 활성 캐릭터가 하나라도 연결된 원작만 조회
|
||||||
*/
|
*/
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
value = """
|
||||||
SELECT ow FROM OriginalWork ow
|
SELECT ow FROM OriginalWork ow
|
||||||
WHERE ow.isDeleted = false
|
WHERE ow.isDeleted = false
|
||||||
AND (:includeAdult = true OR ow.isAdult = false)
|
AND (:includeAdult = true OR ow.isAdult = false)
|
||||||
@@ -48,7 +48,16 @@ interface OriginalWorkRepository : JpaRepository<OriginalWork, Long> {
|
|||||||
WHERE c.originalWork = ow AND c.isActive = true
|
WHERE c.originalWork = ow AND c.isActive = true
|
||||||
)
|
)
|
||||||
ORDER BY ow.createdAt DESC
|
ORDER BY ow.createdAt DESC
|
||||||
|
""",
|
||||||
|
countQuery = """
|
||||||
|
SELECT COUNT(ow) FROM OriginalWork ow
|
||||||
|
WHERE ow.isDeleted = false
|
||||||
|
AND (:includeAdult = true OR ow.isAdult = false)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ChatCharacter c
|
||||||
|
WHERE c.originalWork = ow AND c.isActive = true
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun findAllForApp(@Param("includeAdult") includeAdult: Boolean): List<OriginalWork>
|
fun findAllForAppPage(@Param("includeAdult") includeAdult: Boolean, pageable: Pageable): Page<OriginalWork>
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.OneToMany
|
||||||
|
import javax.persistence.Table
|
||||||
|
import javax.persistence.UniqueConstraint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원작 태그 엔티티 (작품/시리즈 태그와 분리)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["tag"])])
|
||||||
|
class OriginalWorkTag(
|
||||||
|
@Column(nullable = false)
|
||||||
|
val tag: String
|
||||||
|
) : BaseEntity() {
|
||||||
|
@OneToMany(mappedBy = "tag")
|
||||||
|
var tagMappings: MutableList<OriginalWorkTagMapping> = mutableListOf()
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OriginalWork 와 OriginalWorkTag 매핑 엔티티
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
class OriginalWorkTagMapping(
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "original_work_id")
|
||||||
|
val originalWork: OriginalWork,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "tag_id")
|
||||||
|
val tag: OriginalWorkTag
|
||||||
|
) : BaseEntity()
|
@@ -13,6 +13,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
|
|||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,20 +30,23 @@ class OriginalWorkController(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 원작 목록
|
* 원작 목록 (페이징)
|
||||||
* - 로그인 불필요
|
* - 로그인 불필요
|
||||||
* - 본인인증하지 않은 경우 19금 제외
|
* - 본인인증하지 않은 경우 19금 제외
|
||||||
* - 활성 캐릭터가 하나라도 연결된 원작만 노출
|
* - 활성 캐릭터가 하나라도 연결된 원작만 노출
|
||||||
|
* - 요청: page(기본 0), size(기본 20)
|
||||||
* - 반환: totalCount + [imageUrl, title, contentType]
|
* - 반환: totalCount + [imageUrl, title, contentType]
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
fun list(
|
fun list(
|
||||||
|
@RequestParam(defaultValue = "0") page: Int,
|
||||||
|
@RequestParam(defaultValue = "20") size: Int,
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
val includeAdult = member?.auth != null
|
val includeAdult = member?.auth != null
|
||||||
val list = queryService.listForApp(includeAdult)
|
val pageRes = queryService.listForAppPage(includeAdult, page, size)
|
||||||
val content = list.map { OriginalWorkListItemResponse.from(it, imageHost) }
|
val content = pageRes.content.map { OriginalWorkListItemResponse.from(it, imageHost) }
|
||||||
ApiResponse.ok(OriginalWorkListResponse(totalCount = content.size.toLong(), content = content))
|
ApiResponse.ok(OriginalWorkListResponse(totalCount = pageRes.totalElements, content = content))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +54,7 @@ class OriginalWorkController(
|
|||||||
* - 로그인 및 본인인증 필수
|
* - 로그인 및 본인인증 필수
|
||||||
* - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크
|
* - 반환: 이미지, 제목, 콘텐츠 타입, 카테고리, 19금 여부, 작품 소개, 원작 링크
|
||||||
* - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description]
|
* - 해당 원작의 활성 캐릭터 리스트 [imageUrl, name, description]
|
||||||
|
* - 캐릭터는 페이징 적용: 첫 페이지 20개
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
fun detail(
|
fun detail(
|
||||||
@@ -60,7 +65,8 @@ class OriginalWorkController(
|
|||||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
|
||||||
val ow = queryService.getOriginalWork(id)
|
val ow = queryService.getOriginalWork(id)
|
||||||
val characters = queryService.getActiveCharacters(id).map {
|
val pageRes = queryService.getActiveCharactersPage(id, page = 0, size = 20)
|
||||||
|
val characters = pageRes.content.map {
|
||||||
val path = it.imagePath ?: "profile/default-profile.png"
|
val path = it.imagePath ?: "profile/default-profile.png"
|
||||||
Character(
|
Character(
|
||||||
characterId = it.id!!,
|
characterId = it.id!!,
|
||||||
|
@@ -48,7 +48,12 @@ data class OriginalWorkDetailResponse(
|
|||||||
@JsonProperty("category") val category: String,
|
@JsonProperty("category") val category: String,
|
||||||
@JsonProperty("isAdult") val isAdult: Boolean,
|
@JsonProperty("isAdult") val isAdult: Boolean,
|
||||||
@JsonProperty("description") val description: String,
|
@JsonProperty("description") val description: String,
|
||||||
|
@JsonProperty("originalWork") val originalWork: String?,
|
||||||
@JsonProperty("originalLink") val originalLink: String?,
|
@JsonProperty("originalLink") val originalLink: String?,
|
||||||
|
@JsonProperty("writer") val writer: String?,
|
||||||
|
@JsonProperty("studio") val studio: String?,
|
||||||
|
@JsonProperty("originalLinks") val originalLinks: List<String>,
|
||||||
|
@JsonProperty("tags") val tags: List<String>,
|
||||||
@JsonProperty("characters") val characters: List<Character>
|
@JsonProperty("characters") val characters: List<Character>
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -69,9 +74,22 @@ data class OriginalWorkDetailResponse(
|
|||||||
category = entity.category,
|
category = entity.category,
|
||||||
isAdult = entity.isAdult,
|
isAdult = entity.isAdult,
|
||||||
description = entity.description,
|
description = entity.description,
|
||||||
|
originalWork = entity.originalWork,
|
||||||
originalLink = entity.originalLink,
|
originalLink = entity.originalLink,
|
||||||
|
writer = entity.writer,
|
||||||
|
studio = entity.studio,
|
||||||
|
originalLinks = entity.originalLinks.map { it.url },
|
||||||
|
tags = entity.tagMappings.map { it.tag.tag },
|
||||||
characters = characters
|
characters = characters
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앱용: 원작별 활성 캐릭터 페이징 응답 DTO
|
||||||
|
*/
|
||||||
|
data class OriginalWorkCharactersPageResponse(
|
||||||
|
@JsonProperty("totalCount") val totalCount: Long,
|
||||||
|
@JsonProperty("content") val content: List<Character>
|
||||||
|
)
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.original.repository
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface OriginalWorkTagRepository : JpaRepository<OriginalWorkTag, Long> {
|
||||||
|
fun findByTag(tag: String): OriginalWorkTag?
|
||||||
|
}
|
@@ -5,6 +5,9 @@ import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepositor
|
|||||||
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
||||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import org.springframework.data.domain.Page
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.data.domain.Sort
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@@ -18,12 +21,21 @@ class OriginalWorkQueryService(
|
|||||||
private val chatCharacterRepository: ChatCharacterRepository
|
private val chatCharacterRepository: ChatCharacterRepository
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 앱용 원작 목록 조회
|
* 앱용 원작 목록 조회 (페이징)
|
||||||
* @param includeAdult true면 19금 포함, false면 제외
|
* @param includeAdult true면 19금 포함, false면 제외
|
||||||
|
* @param page 페이지 번호(0부터)
|
||||||
|
* @param size 페이지 크기(기본 20, 최대 50)
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun listForApp(includeAdult: Boolean): List<OriginalWork> {
|
fun listForAppPage(includeAdult: Boolean, page: Int = 0, size: Int = 20): Page<OriginalWork> {
|
||||||
return originalWorkRepository.findAllForApp(includeAdult)
|
val safePage = if (page < 0) 0 else page
|
||||||
|
val safeSize = when {
|
||||||
|
size <= 0 -> 20
|
||||||
|
size > 50 -> 50
|
||||||
|
else -> size
|
||||||
|
}
|
||||||
|
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
|
||||||
|
return originalWorkRepository.findAllForAppPage(includeAdult, pageable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,10 +48,21 @@ class OriginalWorkQueryService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 지정 원작에 속한 활성 캐릭터 목록 조회 (최신순)
|
* 지정 원작에 속한 활성 캐릭터 페이징 조회 (최신순)
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getActiveCharacters(originalWorkId: Long): List<ChatCharacter> {
|
fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page<ChatCharacter> {
|
||||||
return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrueOrderByCreatedAtDesc(originalWorkId)
|
// 원작 존재 및 소프트 삭제 여부 확인
|
||||||
|
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
|
||||||
|
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
|
||||||
|
|
||||||
|
val safePage = if (page < 0) 0 else page
|
||||||
|
val safeSize = when {
|
||||||
|
size <= 0 -> 20
|
||||||
|
size > 50 -> 50
|
||||||
|
else -> size
|
||||||
|
}
|
||||||
|
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
|
||||||
|
return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrue(originalWorkId, pageable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,7 +53,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
|||||||
.innerJoin(useCan.room, liveRoom)
|
.innerJoin(useCan.room, liveRoom)
|
||||||
.innerJoin(liveRoom.member, member)
|
.innerJoin(liveRoom.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.createdAt.goe(startDate))
|
.and(useCan.createdAt.goe(startDate))
|
||||||
@@ -119,7 +122,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
|||||||
.innerJoin(order.audioContent, audioContent)
|
.innerJoin(order.audioContent, audioContent)
|
||||||
.innerJoin(audioContent.member, member)
|
.innerJoin(audioContent.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
order.createdAt.goe(startDate)
|
order.createdAt.goe(startDate)
|
||||||
.and(order.createdAt.loe(endDate))
|
.and(order.createdAt.loe(endDate))
|
||||||
@@ -196,7 +202,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
|||||||
.innerJoin(order.audioContent, audioContent)
|
.innerJoin(order.audioContent, audioContent)
|
||||||
.innerJoin(audioContent.member, member)
|
.innerJoin(audioContent.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
audioContent.member.id.eq(memberId)
|
audioContent.member.id.eq(memberId)
|
||||||
.and(order.isActive.isTrue)
|
.and(order.isActive.isTrue)
|
||||||
@@ -318,7 +327,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
|||||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||||
.innerJoin(creatorCommunity.member, member)
|
.innerJoin(creatorCommunity.member, member)
|
||||||
.leftJoin(creatorSettlementRatio)
|
.leftJoin(creatorSettlementRatio)
|
||||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
.on(
|
||||||
|
member.id.eq(creatorSettlementRatio.member.id)
|
||||||
|
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
useCan.isRefund.isFalse
|
useCan.isRefund.isFalse
|
||||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||||
|
Reference in New Issue
Block a user