feat(agent-read): 관리자 에이전트 조회 API를 추가한다

This commit is contained in:
2026-04-10 19:53:31 +09:00
parent 576498ffb6
commit 59a4b06d86
12 changed files with 1761 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
package kr.co.vividnext.sodalive.admin.partner.agent.read
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/partner/agent")
class AdminAgentReadController(
private val service: AdminAgentReadService
) {
@GetMapping("/list")
fun getAgentList(pageable: Pageable) = ApiResponse.ok(
service.getAgentList(offset = pageable.offset, limit = pageable.pageSize.toLong())
)
@GetMapping("/creator/search")
fun searchAssignableCreators(
@RequestParam(value = "search_word") searchWord: String,
pageable: Pageable
) = ApiResponse.ok(
service.searchAssignableCreators(searchWord = searchWord, offset = pageable.offset, limit = pageable.pageSize.toLong())
)
@GetMapping("/{agentId}/creator/list")
fun getAssignedCreators(
@PathVariable agentId: Long,
pageable: Pageable
) = ApiResponse.ok(
service.getAssignedCreators(agentId = agentId, offset = pageable.offset, limit = pageable.pageSize.toLong())
)
@GetMapping("/{agentId}/calculate/live-by-creator")
fun getCalculateLiveByCreator(
@PathVariable agentId: Long,
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateLiveByCreator(
agentId,
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/{agentId}/calculate/content-by-creator")
fun getCalculateContentByCreator(
@PathVariable agentId: Long,
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateContentByCreator(
agentId,
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/{agentId}/calculate/community-by-creator")
fun getCalculateCommunityByCreator(
@PathVariable agentId: Long,
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateCommunityByCreator(
agentId,
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/{agentId}/calculate/channel-donation-by-creator")
fun getChannelDonationByCreator(
@PathVariable agentId: Long,
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getChannelDonationByCreator(
agentId,
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/{agentId}/calculate/content-donation-by-creator")
fun getCalculateContentDonationByCreator(
@PathVariable agentId: Long,
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateContentDonationByCreator(
agentId,
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
}

View File

@@ -0,0 +1,175 @@
package kr.co.vividnext.sodalive.admin.partner.agent.read
import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
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.partner.agent.assignment.QAgentCreatorRelation.agentCreatorRelation
import kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateQueryRepository
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
@Repository
class AdminAgentReadQueryRepository(
private val queryFactory: JPAQueryFactory,
private val calculateQueryRepository: AgentCalculateQueryRepository
) {
private val kstZoneId = ZoneId.of("Asia/Seoul")
private val utcZoneId = ZoneId.of("UTC")
fun getAgentListTotalCount(): Int {
return queryFactory
.select(member.id.count())
.from(member)
.where(member.role.eq(MemberRole.AGENT))
.fetchOne()
?.toInt()
?: 0
}
fun getAgentList(offset: Long, limit: Long, currentTime: ZonedDateTime): List<GetAdminAgentListItem> {
val kstCurrentTime = currentTime.withZoneSameInstant(kstZoneId)
val currentMonthStartKst = kstCurrentTime.toLocalDate().withDayOfMonth(1).atStartOfDay(kstZoneId)
val nextMonthStartKst = currentMonthStartKst.plusMonths(1)
val currentMonthStart = currentMonthStartKst.withZoneSameInstant(utcZoneId).toLocalDateTime()
val currentMonthEnd = nextMonthStartKst.withZoneSameInstant(utcZoneId).toLocalDateTime().minusNanos(1)
return queryFactory
.select(
QGetAdminAgentListItem(
member.id,
member.nickname,
agentCreatorRelation.id.countDistinct().intValue(),
Expressions.numberTemplate(Int::class.java, "0"),
Expressions.numberTemplate(Int::class.java, "0"),
Expressions.numberTemplate(Int::class.java, "0"),
Expressions.numberTemplate(Int::class.java, "0"),
Expressions.numberTemplate(Int::class.java, "0")
)
)
.from(member)
.leftJoin(agentCreatorRelation)
.on(
agentCreatorRelation.agent.id.eq(member.id)
.and(isActiveAssignment(kstCurrentTime.toLocalDateTime()))
)
.where(member.role.eq(MemberRole.AGENT))
.groupBy(member.id, member.nickname)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
.map { item ->
item.copy(
liveAgentSettlementAmount = calculateQueryRepository
.getCalculateLiveByCreatorTotal(currentMonthStart, currentMonthEnd, item.agentId)
.agentSettlementAmount,
contentAgentSettlementAmount = calculateQueryRepository
.getCalculateContentByCreatorTotal(currentMonthStart, currentMonthEnd, item.agentId)
.agentSettlementAmount,
communityAgentSettlementAmount = calculateQueryRepository
.getCalculateCommunityByCreatorTotal(currentMonthStart, currentMonthEnd, item.agentId)
.agentSettlementAmount,
contentDonationAgentSettlementAmount = calculateQueryRepository
.getCalculateContentDonationByCreatorTotal(currentMonthStart, currentMonthEnd, item.agentId)
.agentSettlementAmount,
channelDonationAgentSettlementAmount = calculateQueryRepository
.getChannelDonationByCreatorTotal(currentMonthStart, currentMonthEnd, item.agentId)
.agentSettlementAmount
)
}
}
fun searchAssignableCreatorsTotalCount(searchWord: String, currentTime: LocalDateTime): Int {
return queryFactory
.select(member.id.countDistinct())
.from(member)
.leftJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(isActiveAssignment(currentTime))
)
.where(
member.role.eq(MemberRole.CREATOR)
.and(member.nickname.contains(searchWord))
)
.fetchOne()
?.toInt()
?: 0
}
fun searchAssignableCreators(
searchWord: String,
offset: Long,
limit: Long,
currentTime: LocalDateTime
): List<SearchAdminAgentAssignableCreatorItem> {
val currentAgent = QMember("currentAgent")
return queryFactory
.select(
QSearchAdminAgentAssignableCreatorItem(
member.id,
member.nickname,
currentAgent.id,
currentAgent.nickname
)
)
.from(member)
.leftJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(isActiveAssignment(currentTime))
)
.leftJoin(agentCreatorRelation.agent, currentAgent)
.where(
member.role.eq(MemberRole.CREATOR)
.and(member.nickname.contains(searchWord))
)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
fun getAssignedCreatorTotalCount(agentId: Long, currentTime: LocalDateTime): Int {
return queryFactory
.select(agentCreatorRelation.id.count())
.from(agentCreatorRelation)
.where(agentCreatorRelation.agent.id.eq(agentId).and(isActiveAssignment(currentTime)))
.fetchOne()
?.toInt()
?: 0
}
fun getAssignedCreators(
agentId: Long,
offset: Long,
limit: Long,
currentTime: LocalDateTime
): List<GetAdminAgentAssignedCreatorItem> {
return queryFactory
.select(
QGetAdminAgentAssignedCreatorItem(
agentCreatorRelation.creator.id,
agentCreatorRelation.creator.nickname,
agentCreatorRelation.assignedAt
)
)
.from(agentCreatorRelation)
.where(agentCreatorRelation.agent.id.eq(agentId).and(isActiveAssignment(currentTime)))
.orderBy(agentCreatorRelation.creator.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
private fun isActiveAssignment(currentTime: LocalDateTime): BooleanExpression {
return agentCreatorRelation.assignedAt.loe(currentTime)
.and(agentCreatorRelation.unassignedAt.isNull.or(agentCreatorRelation.unassignedAt.gt(currentTime)))
}
}

View File

@@ -0,0 +1,126 @@
package kr.co.vividnext.sodalive.admin.partner.agent.read
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateService
import kr.co.vividnext.sodalive.partner.agent.calculate.GetAgentChannelDonationSettlementByCreatorResponse
import kr.co.vividnext.sodalive.partner.agent.calculate.GetAgentSettlementByCreatorResponse
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
@Service
class AdminAgentReadService(
private val queryRepository: AdminAgentReadQueryRepository,
private val memberRepository: MemberRepository,
private val calculateService: AgentCalculateService
) {
private val kstZoneId = ZoneId.of("Asia/Seoul")
@Transactional(readOnly = true)
fun getAgentList(offset: Long, limit: Long): GetAdminAgentListResponse {
val now = ZonedDateTime.now(kstZoneId)
return GetAdminAgentListResponse(
totalCount = queryRepository.getAgentListTotalCount(),
items = queryRepository.getAgentList(offset = offset, limit = limit, currentTime = now)
)
}
@Transactional(readOnly = true)
fun searchAssignableCreators(searchWord: String, offset: Long, limit: Long): SearchAdminAgentAssignableCreatorResponse {
if (searchWord.length < 2) throw SodaException(messageKey = "admin.member.search_word_min_length")
val now = LocalDateTime.now()
return SearchAdminAgentAssignableCreatorResponse(
totalCount = queryRepository.searchAssignableCreatorsTotalCount(searchWord = searchWord, currentTime = now),
items = queryRepository.searchAssignableCreators(
searchWord = searchWord,
offset = offset,
limit = limit,
currentTime = now
)
)
}
@Transactional(readOnly = true)
fun getAssignedCreators(agentId: Long, offset: Long, limit: Long): GetAdminAgentAssignedCreatorResponse {
validateAgent(agentId)
val now = LocalDateTime.now()
return GetAdminAgentAssignedCreatorResponse(
totalCount = queryRepository.getAssignedCreatorTotalCount(agentId = agentId, currentTime = now),
items = queryRepository.getAssignedCreators(agentId = agentId, offset = offset, limit = limit, currentTime = now)
)
}
@Transactional(readOnly = true)
fun getCalculateLiveByCreator(
agentId: Long,
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
validateAgent(agentId)
return calculateService.getCalculateLiveByCreator(startDateStr, endDateStr, agentId, offset, limit)
}
@Transactional(readOnly = true)
fun getCalculateContentByCreator(
agentId: Long,
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
validateAgent(agentId)
return calculateService.getCalculateContentByCreator(startDateStr, endDateStr, agentId, offset, limit)
}
@Transactional(readOnly = true)
fun getCalculateCommunityByCreator(
agentId: Long,
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
validateAgent(agentId)
return calculateService.getCalculateCommunityByCreator(startDateStr, endDateStr, agentId, offset, limit)
}
@Transactional(readOnly = true)
fun getCalculateContentDonationByCreator(
agentId: Long,
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
validateAgent(agentId)
return calculateService.getCalculateContentDonationByCreator(startDateStr, endDateStr, agentId, offset, limit)
}
@Transactional(readOnly = true)
fun getChannelDonationByCreator(
agentId: Long,
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetAgentChannelDonationSettlementByCreatorResponse {
validateAgent(agentId)
return calculateService.getChannelDonationByCreator(startDateStr, endDateStr, agentId, offset, limit)
}
private fun validateAgent(agentId: Long) {
val member = memberRepository.findById(agentId).orElseThrow {
SodaException(messageKey = "partner.agent.ratio.agent_not_found")
}
if (member.role != MemberRole.AGENT) {
throw SodaException(messageKey = "partner.agent.ratio.invalid_agent")
}
}
}

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.admin.partner.agent.read
import com.querydsl.core.annotations.QueryProjection
import java.time.LocalDateTime
data class GetAdminAgentAssignedCreatorResponse(
val totalCount: Int,
val items: List<GetAdminAgentAssignedCreatorItem>
)
data class GetAdminAgentAssignedCreatorItem @QueryProjection constructor(
val creatorId: Long,
val creatorNickname: String,
val assignedAt: LocalDateTime
)

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.partner.agent.read
import com.querydsl.core.annotations.QueryProjection
data class GetAdminAgentListResponse(
val totalCount: Int,
val items: List<GetAdminAgentListItem>
)
data class GetAdminAgentListItem @QueryProjection constructor(
val agentId: Long,
val agentNickname: String,
val assignedCreatorCount: Int,
val liveAgentSettlementAmount: Int = 0,
val contentAgentSettlementAmount: Int = 0,
val communityAgentSettlementAmount: Int = 0,
val contentDonationAgentSettlementAmount: Int = 0,
val channelDonationAgentSettlementAmount: Int = 0
)

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.admin.partner.agent.read
import com.querydsl.core.annotations.QueryProjection
data class SearchAdminAgentAssignableCreatorResponse(
val totalCount: Int,
val items: List<SearchAdminAgentAssignableCreatorItem>
)
data class SearchAdminAgentAssignableCreatorItem @QueryProjection constructor(
val creatorId: Long,
val creatorNickname: String,
val currentAgentId: Long?,
val currentAgentNickname: String?
)