feat(agent-calculate): 소속 크리에이터 응답에 프로필 이미지를 추가한다

This commit is contained in:
2026-04-13 18:51:33 +09:00
parent f740041dca
commit 2c19e4b76c
9 changed files with 75 additions and 11 deletions

View File

@@ -0,0 +1,20 @@
# 에이전트 소속 크리에이터 프로필 이미지 추가
- [x] `getAssignedCreators` 흐름과 기존 프로필 이미지 응답 패턴을 확인한다.
- [x] `GetAgentAssignedCreatorItem`에 프로필 이미지 URL 필드를 추가한다.
- [x] `AgentCalculateQueryRepository.getAssignedCreators` projection에 프로필 이미지 URL을 포함한다.
- [x] `AgentCalculateQueryRepositoryTest`에 프로필 이미지 URL 및 기본 이미지 fallback 검증을 추가한다.
- [x] `AgentCalculateServiceTest`, `AgentCalculateControllerTest` fixture와 검증을 갱신한다.
- [x] 정적 진단과 관련 테스트를 실행해 변경을 검증한다.
## 검증 기록
### 1차 구현
- 무엇을: 에이전트 소속 크리에이터 조회 응답에 프로필 이미지 URL 필드 추가 구현 및 검증
- 왜: 크리에이터 목록 응답에서 프로필 이미지를 함께 내려주기 위해
- 어떻게:
- `./gradlew test --tests "kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateServiceTest"` 실행 시 `profileImageUrl``cloudFrontHost` 관련 컴파일 실패를 확인해 red 단계 검증
- `./gradlew test --tests "kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateQueryRepositoryTest" --tests "kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateServiceTest" --tests "kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew test --tests "kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateQueryRepositoryTest.shouldGetAssignedCreatorsWithProfileImageUrl" --tests "kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateControllerTest.shouldForwardAssignedCreatorsRequestToService"` 실행 결과 `BUILD SUCCESSFUL`
- Kotlin LSP 서버가 현재 환경에 없어 `lsp_diagnostics`는 수행 불가였고, 대신 compile/test/ktlint 결과로 정적 검증 대체

View File

@@ -15,6 +15,7 @@ import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.QMember.member 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.assignment.QAgentCreatorRelation.agentCreatorRelation
import kr.co.vividnext.sodalive.partner.agent.ratio.QAgentSettlementRatio.agentSettlementRatio import kr.co.vividnext.sodalive.partner.agent.ratio.QAgentSettlementRatio.agentSettlementRatio
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.persistence.EntityManager import javax.persistence.EntityManager
@@ -22,7 +23,10 @@ import javax.persistence.EntityManager
@Repository @Repository
class AgentCalculateQueryRepository( class AgentCalculateQueryRepository(
private val queryFactory: JPAQueryFactory, private val queryFactory: JPAQueryFactory,
private val entityManager: EntityManager private val entityManager: EntityManager,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) { ) {
fun getAssignedCreatorTotalCount(agentId: Long, currentTime: LocalDateTime): Int { fun getAssignedCreatorTotalCount(agentId: Long, currentTime: LocalDateTime): Int {
return queryFactory return queryFactory
@@ -46,7 +50,11 @@ class AgentCalculateQueryRepository(
.select( .select(
QGetAgentAssignedCreatorItem( QGetAgentAssignedCreatorItem(
agentCreatorRelation.creator.id, agentCreatorRelation.creator.id,
agentCreatorRelation.creator.nickname agentCreatorRelation.creator.nickname,
agentCreatorRelation.creator.profileImage
.coalesce("profile/default-profile.png")
.prepend("/")
.prepend(cloudFrontHost)
) )
) )
.from(agentCreatorRelation) .from(agentCreatorRelation)

View File

@@ -9,5 +9,6 @@ data class GetAgentAssignedCreatorResponse(
data class GetAgentAssignedCreatorItem @QueryProjection constructor( data class GetAgentAssignedCreatorItem @QueryProjection constructor(
val creatorId: Long, val creatorId: Long,
val creatorNickname: String val creatorNickname: String,
val profileImageUrl: String
) )

View File

@@ -58,12 +58,14 @@ class AdminAgentReadCurrentMonthListSummaryTest @Autowired constructor(
private val snapshotRepository: AgentSettlementSnapshotRepository, private val snapshotRepository: AgentSettlementSnapshotRepository,
private val entityManager: EntityManager private val entityManager: EntityManager
) { ) {
private val cloudFrontHost = "https://cdn.test"
private lateinit var repository: AdminAgentReadQueryRepository private lateinit var repository: AdminAgentReadQueryRepository
private lateinit var calculateService: AgentCalculateService private lateinit var calculateService: AgentCalculateService
@BeforeEach @BeforeEach
fun setup() { fun setup() {
val calculateQueryRepository = AgentCalculateQueryRepository(queryFactory, entityManager) val calculateQueryRepository = AgentCalculateQueryRepository(queryFactory, entityManager, cloudFrontHost)
repository = AdminAgentReadQueryRepository(queryFactory, calculateQueryRepository) repository = AdminAgentReadQueryRepository(queryFactory, calculateQueryRepository)
calculateService = AgentCalculateService(calculateQueryRepository, snapshotRepository) calculateService = AgentCalculateService(calculateQueryRepository, snapshotRepository)
registerMysqlDateFunctions() registerMysqlDateFunctions()

View File

@@ -55,6 +55,8 @@ class AdminAgentReadParityTest @Autowired constructor(
private val snapshotRepository: AgentSettlementSnapshotRepository, private val snapshotRepository: AgentSettlementSnapshotRepository,
private val entityManager: EntityManager private val entityManager: EntityManager
) { ) {
private val cloudFrontHost = "https://cdn.test"
private var seededAgentId: Long = 0L private var seededAgentId: Long = 0L
private lateinit var agentService: AgentCalculateService private lateinit var agentService: AgentCalculateService
private lateinit var adminService: AdminAgentReadService private lateinit var adminService: AdminAgentReadService
@@ -62,7 +64,7 @@ class AdminAgentReadParityTest @Autowired constructor(
@BeforeEach @BeforeEach
fun setup() { fun setup() {
registerMysqlDateFunctions() registerMysqlDateFunctions()
val queryRepository = AgentCalculateQueryRepository(queryFactory, entityManager) val queryRepository = AgentCalculateQueryRepository(queryFactory, entityManager, cloudFrontHost)
val adminQueryRepository = AdminAgentReadQueryRepository(queryFactory, queryRepository) val adminQueryRepository = AdminAgentReadQueryRepository(queryFactory, queryRepository)
agentService = AgentCalculateService(queryRepository, snapshotRepository) agentService = AgentCalculateService(queryRepository, snapshotRepository)
adminService = AdminAgentReadService(adminQueryRepository, memberRepository, agentService) adminService = AdminAgentReadService(adminQueryRepository, memberRepository, agentService)

View File

@@ -28,13 +28,15 @@ class AdminAgentReadQueryRepositoryTest @Autowired constructor(
private val relationRepository: AgentCreatorRelationRepository, private val relationRepository: AgentCreatorRelationRepository,
private val entityManager: EntityManager private val entityManager: EntityManager
) { ) {
private val cloudFrontHost = "https://cdn.test"
private lateinit var repository: AdminAgentReadQueryRepository private lateinit var repository: AdminAgentReadQueryRepository
@BeforeEach @BeforeEach
fun setup() { fun setup() {
repository = AdminAgentReadQueryRepository( repository = AdminAgentReadQueryRepository(
queryFactory, queryFactory,
AgentCalculateQueryRepository(queryFactory, entityManager) AgentCalculateQueryRepository(queryFactory, entityManager, cloudFrontHost)
) )
} }

View File

@@ -43,7 +43,8 @@ class AgentCalculateControllerTest {
items = listOf( items = listOf(
GetAgentAssignedCreatorItem( GetAgentAssignedCreatorItem(
creatorId = 21L, creatorId = 21L,
creatorNickname = "creator-a" creatorNickname = "creator-a",
profileImageUrl = "https://cdn.test/profile/creator-a.png"
) )
) )
) )
@@ -58,6 +59,7 @@ class AgentCalculateControllerTest {
assertEquals(true, apiResponse.success) assertEquals(true, apiResponse.success)
assertEquals(1, apiResponse.data!!.totalCount) assertEquals(1, apiResponse.data!!.totalCount)
assertEquals(21L, apiResponse.data!!.items[0].creatorId) assertEquals(21L, apiResponse.data!!.items[0].creatorId)
assertEquals("https://cdn.test/profile/creator-a.png", apiResponse.data!!.items[0].profileImageUrl)
Mockito.verify(service).getAssignedCreators(agentId = 7L, offset = 10L, limit = 5L) Mockito.verify(service).getAssignedCreators(agentId = 7L, offset = 10L, limit = 5L)
} }

View File

@@ -56,13 +56,15 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor(
private val useCanCalculateRepository: UseCanCalculateRepository, private val useCanCalculateRepository: UseCanCalculateRepository,
private val entityManager: EntityManager private val entityManager: EntityManager
) { ) {
private val cloudFrontHost = "https://cdn.test"
private lateinit var repository: AgentCalculateQueryRepository private lateinit var repository: AgentCalculateQueryRepository
private lateinit var service: AgentCalculateService private lateinit var service: AgentCalculateService
@BeforeEach @BeforeEach
fun setup() { fun setup() {
registerMysqlDateFunctions() registerMysqlDateFunctions()
repository = AgentCalculateQueryRepository(queryFactory, entityManager) repository = AgentCalculateQueryRepository(queryFactory, entityManager, cloudFrontHost)
service = AgentCalculateService(repository, snapshotRepository) service = AgentCalculateService(repository, snapshotRepository)
} }
@@ -110,6 +112,28 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor(
assertEquals(listOf(endingFutureCreator.id, currentCreator.id), items.map { it.creatorId }) assertEquals(listOf(endingFutureCreator.id, currentCreator.id), items.map { it.creatorId })
} }
@Test
@DisplayName("소속 크리에이터 목록 조회는 프로필 이미지 URL과 기본 이미지를 함께 반환한다")
fun shouldGetAssignedCreatorsWithProfileImageUrl() {
val agent = saveMember("agent-image", MemberRole.AGENT)
val creatorWithImage = saveMember("creator-image", MemberRole.CREATOR, profileImage = "profile/creator-image.png")
val creatorWithoutImage = saveMember("creator-default", MemberRole.CREATOR)
val currentTime = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
saveRelation(agent, creatorWithImage)
saveRelation(agent, creatorWithoutImage)
val items = repository.getAssignedCreators(agent.id!!, offset = 0, limit = 10, currentTime = currentTime)
assertEquals(
listOf(
"$cloudFrontHost/profile/default-profile.png",
"$cloudFrontHost/profile/creator-image.png"
),
items.map { it.profileImageUrl }
)
}
@Test @Test
@DisplayName("라이브 크리에이터별 조회는 소속된 크리에이터만 집계한다") @DisplayName("라이브 크리에이터별 조회는 소속된 크리에이터만 집계한다")
fun shouldGetLiveSummaryRowsOnlyForAssignedCreators() { fun shouldGetLiveSummaryRowsOnlyForAssignedCreators() {
@@ -683,13 +707,14 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor(
assertEquals(zeroTotal, repository.getCalculateContentDonationByCreatorTotal(startDate, endDate, agent.id!!)) assertEquals(zeroTotal, repository.getCalculateContentDonationByCreatorTotal(startDate, endDate, agent.id!!))
} }
private fun saveMember(nickname: String, role: MemberRole): Member { private fun saveMember(nickname: String, role: MemberRole, profileImage: String? = null): Member {
return memberRepository.saveAndFlush( return memberRepository.saveAndFlush(
Member( Member(
email = "$nickname@test.com", email = "$nickname@test.com",
password = "password", password = "password",
nickname = nickname, nickname = nickname,
role = role role = role,
profileImage = profileImage
) )
) )
} }

View File

@@ -41,7 +41,8 @@ class AgentCalculateServiceTest {
val items = listOf( val items = listOf(
GetAgentAssignedCreatorItem( GetAgentAssignedCreatorItem(
creatorId = 21L, creatorId = 21L,
creatorNickname = "creator-a" creatorNickname = "creator-a",
profileImageUrl = "https://cdn.test/profile/creator-a.png"
) )
) )
@@ -64,6 +65,7 @@ class AgentCalculateServiceTest {
assertEquals(1, response.totalCount) assertEquals(1, response.totalCount)
assertEquals(21L, response.items[0].creatorId) assertEquals(21L, response.items[0].creatorId)
assertEquals("https://cdn.test/profile/creator-a.png", response.items[0].profileImageUrl)
Mockito.verify(repository).getAssignedCreatorTotalCount( Mockito.verify(repository).getAssignedCreatorTotalCount(
Mockito.eq(7L), Mockito.eq(7L),
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now() Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()