9.3 KiB
9.3 KiB
openRoom 응답 상대방 프로필/닉네임 추가 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development또는superpowers:executing-plans로 task 단위 구현을 진행한다. 각 단계는 체크박스(- [ ])로 진행 상태를 갱신한다.
Goal: GET /api/v2/user-creator-chat/rooms/{roomId}/open 응답에 현재 로그인 회원 기준 상대방 닉네임과 프로필 이미지 URL을 추가한다.
Architecture: 기존 UserCreatorChatService.openRoom 흐름을 유지하고, 이미 존재하는 UserCreatorChatParticipantRepository.findActiveOpponent(roomId, memberId)로 상대방 참여자를 조회한다. 응답 DTO인 UserCreatorChatRoomOpenResponse에 opponentNickname, opponentProfileImageUrl만 추가하며, 메시지 조회/페이징/참여자 검증 정책은 변경하지 않는다.
Tech Stack: Kotlin, Spring Boot 2.7.14, Java 17, JUnit 5, Mockito, Gradle Wrapper
0. 구현 전 확정 사항
- 대상 PRD:
docs/20260610_openRoom_상대방_프로필_닉네임/prd.md - 대상 API:
GET /api/v2/user-creator-chat/rooms/{roomId}/open - 응답 DTO:
UserCreatorChatRoomOpenResponse - 신규 응답 필드:
opponentNickname: StringopponentProfileImageUrl: String
- 상대방 기준: 현재 로그인 회원을 제외한 활성 참여자
- 프로필 이미지 URL 정책:
"$cloudFrontHost/$profilePath" - 기본 프로필 이미지 경로:
profile/default-profile.png - 기존 응답 필드(
roomId,messages,hasMore,nextCursor)는 제거/이름 변경하지 않는다. createOrGetRoom,getMessages, 메시지 발송 API 응답은 변경하지 않는다.- DB 스키마 변경은 하지 않는다.
1. 파일 구조 계획
수정 대상
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/dto/UserCreatorChatDtos.ktUserCreatorChatRoomOpenResponse에 상대방 표시 필드 2개를 추가한다.
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.ktopenRoom에서 상대방 참여자를 조회하고 응답 필드를 채운다.- 기존 메시지 DTO와 같은 기본 프로필 이미지 URL 조합 정책을 유지한다.
- Modify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.ktopenRoom응답에 상대방 닉네임/프로필 이미지 URL이 포함되는지 검증한다.- 상대방 프로필 이미지가
null일 때 기본 이미지 URL을 반환하는지 검증한다.
신규 파일
- 없음.
Phase 1: openRoom 응답 확장 TDD
- Task 1.1: openRoom 응답에 상대방 닉네임과 프로필 이미지 URL 추가
- Files:
- Modify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/dto/UserCreatorChatDtos.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt
- Modify:
- RED: 기존
UserCreatorChatServiceTest에 아래 테스트 2개를 추가한다.shouldOpenRoomWithOpponentProfileWhenOpponentHasProfileImageshouldOpenRoomWithDefaultOpponentProfileWhenOpponentProfileImageIsNull
- RED 테스트 코드 예시:
- Files:
@Test
@DisplayName("방 입장 응답은 상대방 닉네임과 프로필 이미지 URL을 포함한다")
fun shouldOpenRoomWithOpponentProfileWhenOpponentHasProfileImage() {
val user = member(1L, "user")
val creator = member(2L, "creator").apply {
profileImage = "profile/creator.png"
}
val room = room(10L)
val userParticipant = participant(100L, room, user)
val creatorParticipant = participant(101L, room, creator)
Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room)
Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant)
Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(creatorParticipant)
Mockito.`when`(
messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(
room,
PageRequest.of(0, 20)
)
).thenReturn(emptyList())
val response = service.openRoom(user, roomId = 10L)
assertEquals(10L, response.roomId)
assertEquals("creator", response.opponentNickname)
assertEquals("https://cdn.test/profile/creator.png", response.opponentProfileImageUrl)
assertEquals(emptyList<UserCreatorChatMessageItemDto>(), response.messages)
assertFalse(response.hasMore)
assertEquals(null, response.nextCursor)
Mockito.verify(participantRepository).findActiveOpponent(10L, 1L)
}
@Test
@DisplayName("상대방 프로필 이미지가 없으면 방 입장 응답은 기본 프로필 이미지 URL을 반환한다")
fun shouldOpenRoomWithDefaultOpponentProfileWhenOpponentProfileImageIsNull() {
val user = member(1L, "user")
val creator = member(2L, "creator")
val room = room(10L)
val userParticipant = participant(100L, room, user)
val creatorParticipant = participant(101L, room, creator)
Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room)
Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant)
Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(creatorParticipant)
Mockito.`when`(
messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(
room,
PageRequest.of(0, 20)
)
).thenReturn(emptyList())
val response = service.openRoom(user, roomId = 10L)
assertEquals("creator", response.opponentNickname)
assertEquals("https://cdn.test/profile/default-profile.png", response.opponentProfileImageUrl)
}
- 실패 확인:
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest - Expected:
opponentNickname또는opponentProfileImageUrl프로퍼티가 없어 컴파일 실패한다.
- Run:
- GREEN:
UserCreatorChatRoomOpenResponse생성자에opponentNickname,opponentProfileImageUrl을 추가한다. - GREEN 구현 방향:
data class UserCreatorChatRoomOpenResponse(
val roomId: Long,
val opponentNickname: String,
val opponentProfileImageUrl: String,
val messages: List<UserCreatorChatMessageItemDto>,
val hasMore: Boolean,
val nextCursor: Long?
)
- GREEN:
UserCreatorChatService.openRoom에서 기존 참여자 검증 후 상대방을 조회해 응답에 채운다. - GREEN 구현 방향:
@Transactional
fun openRoom(member: Member, roomId: Long, limit: Int = 20): UserCreatorChatRoomOpenResponse {
val room = findRoom(roomId)
requireParticipant(roomId, member.id!!)
val opponentParticipant = participantRepository.findActiveOpponent(roomId, member.id!!)
?: throw SodaException(messageKey = "chat.room.invalid_access")
val opponent = opponentParticipant.member
val opponentProfilePath = opponent.profileImage ?: "profile/default-profile.png"
val page = getMessages(member, roomId, cursor = null, limit = limit)
return UserCreatorChatRoomOpenResponse(
roomId = room.id!!,
opponentNickname = opponent.nickname,
opponentProfileImageUrl = "$cloudFrontHost/$opponentProfilePath",
messages = page.messages,
hasMore = page.hasMore,
nextCursor = page.nextCursor
)
}
- 통과 확인:
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest - Expected:
BUILD SUCCESSFUL
- Run:
- REFACTOR: 기본 프로필 이미지 문자열 중복이 과하다고 판단되면
UserCreatorChatService안에 private 함수로만 정리한다. 새 공용 유틸이나 별도 추상화는 만들지 않는다. - REFACTOR 후보:
private fun profileImageUrl(profileImage: String?): String {
val profilePath = profileImage ?: "profile/default-profile.png"
return "$cloudFrontHost/$profilePath"
}
- 회귀 확인:
- Run:
./gradlew test - Expected:
BUILD SUCCESSFUL - Run:
./gradlew ktlintCheck - Expected:
BUILD SUCCESSFUL
- Run:
- 기대 결과:
- 메시지가 없는 방에서도
opponentNickname,opponentProfileImageUrl이 반환된다. - 상대방 프로필 이미지가 있으면 CloudFront URL로 반환된다.
- 상대방 프로필 이미지가 없으면
https://cdn.test/profile/default-profile.png형식으로 반환된다. - 기존 메시지 조회, 페이징, 참여자 검증 동작은 유지된다.
- 메시지가 없는 방에서도
2. 구현 후 검증 기록
./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest- 목적: RED 확인.
openRoom상대방 닉네임/프로필 이미지 URL 테스트를 먼저 추가한 뒤 실행. - 결과:
opponentNickname,opponentProfileImageUrl미정의 컴파일 오류로 실패.
- 목적: RED 확인.
./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest- 목적: GREEN 확인. DTO/서비스 최소 구현 후
UserCreatorChatServiceTest회귀 검증. - 결과:
BUILD SUCCESSFUL.
- 목적: GREEN 확인. DTO/서비스 최소 구현 후
./gradlew ktlintCheck- 목적: Kotlin lint 회귀 검증.
- 결과:
BUILD SUCCESSFUL.
./gradlew test- 목적: 전체 테스트 회귀 검증.
- 결과:
BUILD SUCCESSFUL.