feat(agent-calculate): 소속 크리에이터 응답에 프로필 이미지를 추가한다
This commit is contained in:
20
docs/20260413_에이전트소속크리에이터프로필이미지추가.md
Normal file
20
docs/20260413_에이전트소속크리에이터프로필이미지추가.md
Normal 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 결과로 정적 검증 대체
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user