112 KiB
메인 홈 팔로잉 탭 API Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development또는superpowers:executing-plans로 task 단위 구현을 진행한다. 각 단계는 체크박스(- [ ])로 진행 상태를 갱신한다.
Goal: GET /api/v2/home/following으로 메인 홈 팔로잉 탭의 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 최근 소식을 조회한다.
Architecture: 공개 API controller/facade/response DTO는 kr.co.vividnext.sodalive.v2.api.home.following 조립 계층에 둔다. 팔로잉 탭 조회 service, 최근 소식 publish service, domain model, port, QueryDSL/JPA repository는 kr.co.vividnext.sodalive.v2.home.following 하위에 두고 v2.api.*에 의존하지 않는다. 최근 소식은 별도 inbox table에 사용자별 row를 저장하고, 이번 범위에서는 외부 MQ/outbox/worker 없이 내부 publish service에서 follower 조회와 bulk insert를 수행한다.
Tech Stack: Kotlin, Spring Boot 2.7.14, Java 17, Spring MVC, Spring Security, Spring Data JPA, QueryDSL, MySQL, JUnit 5, MockMvc, Gradle Wrapper
현재 후속 작업 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---|---|---|---|---|
| 1~2 | 완료 | 기존 Task 전체 | 없음 | 없음 |
| 3 | 완료 | 기존 6/6, 회귀 1/1 |
없음 | 없음 |
| 4 | 완료 | 기존 5/5, 회귀 2/2 |
없음 | 없음 |
| 5~5.5 | 완료 | 기존 Task 전체 | 없음 | 없음 |
| 6 | 완료 | 기존 2/2, 회귀 2/2 |
없음 | 없음 |
| 7 | 완료 | 기존 1/1, 회귀 2/2 |
없음 | 없음 |
- 2026-07-30 1차 Phase별 리뷰의 확정 항목은
P3-R1→P3-R-GATE→P7-R1→P7-R2→P7-R-GATE순서로 실행 완료했다. - 2026-07-30 2차 정적 리뷰의 후속 순서
P4-R1→P4-R-GATE→P6-R1→P6-R-GATE는 실행 완료했다. - 2026-07-30 3차 정적 리뷰의 후속 순서
P4-R2→P4-R2-GATE→P6-R2→P6-R2-GATE는 실행 완료했다.
0. 확정 사항
- API endpoint:
GET /api/v2/home/following - 인증 정책: 비로그인 조회 허용. 비로그인 응답은
isLoginRequired = true와 빈 섹션 배열을 내려준다. - 로그인 회원 응답은
isLoginRequired = false와 팔로잉 탭 데이터를 내려준다. - 응답 wrapper:
ApiResponse.ok(...) SecurityConfig에GET /api/v2/home/followingpermitAll 설정을 추가한다.- 섹션별 기본 노출 수:
followingCreators: 오래된 팔로우순 20개onAirLives: 팔로잉 크리에이터의 현재 진행 중인 라이브 최신순 10개recentChats: DM/AI 채팅 최신순 10개monthlySchedules: 이번 달 오늘 이후 일정 중 오늘과 가까운 순 3개recentNews:visibleFromAtUtc desc,newsId desc기준 30개
- 최근 대화는 기존
ChatRoomListService.getRooms(member, filter = "ALL", cursor = null, limit = 10)와ChatRoomListItemResponse를 재사용한다. - 최근 소식 타입은
CREATOR_RANKING,CONTENT_RANKING,COMMUNITY_POST,AUDIO_CONTENT,PHOTO_CONTENT를 정의한다. - 이번 범위에서 생성하는 랭킹 소식은
CREATOR_RANKING만이다.CONTENT_RANKING은 향후 확장용으로 enum/table 값만 예약한다. - 최근 소식 응답 최상위에는
newsId,type,visibleFromAtUtc만 공통으로 내려준다. - 최근 소식 상세 값은 타입별 nullable nested DTO로 내려준다.
type과 일치하는 nested DTO만 non-null이고 나머지는null이다. CREATOR_RANKING은creatorRanking.rank,creatorRanking.creatorId,creatorRanking.nickname,creatorRanking.profileImageUrl을 사용한다.rankChange,isNew는 사용하지 않는다.CONTENT_RANKING은contentRanking.rank,contentRanking.contentId,contentRanking.contentImageUrl,contentRanking.title을 사용한다.CREATOR_RANKING은 현재 시점에 공개된 최신WEEKLY,DONE크리에이터 랭킹 job 기준 배치만 최근 소식에 표시한다. 신규 배치 공개 전에는 직전 공개 배치를 유지하고, 공개 후에는 이전 배치를 표시하지 않는다.- 최신 공개 배치에 포함되지 않은 팔로잉 크리에이터의 과거
CREATOR_RANKING은 보충하지 않는다. CONTENT_RANKING은 같은contentId의 노출 가능한 row 중visibleFromAtUtc desc,newsId desc기준 최신 항목 하나만 표시한다.- 랭킹 배치 필터와 콘텐츠 중복 제거를 먼저 적용한 뒤 전체 최근 소식 최대 30개를 조회하며, 제외 후 30개 미만이어도 과거·중복 랭킹으로 보충하지 않는다.
CONTENT_RANKINGinbox 발행과 콘텐츠 랭킹 스냅샷 연동은 이번 보완 범위에 포함하지 않는다.AUDIO_CONTENT,PHOTO_CONTENT는 각각audioContent/photoContent에contentId,contentImageUrl,title,creatorProfileImageUrl,creatorNickname을 담고, 공개 시각은 최상위visibleFromAtUtc를 사용한다.COMMUNITY_POST는communityPost에postId,creatorProfileImage,creatorNickname, nullableimageUrl,content, UTCcreatedAt,likeCount,commentCount를 담는다.COMMUNITY_POST최근 소식은 무료 커뮤니티 게시글만 발행한다. 유료 커뮤니티 게시글은 inbox row를 생성하지 않는다.- 최근 소식 inbox table DDL은
docs/20260625_메인_홈_팔로잉_탭_API/create-home-following-news-inbox-table.sql을 기준으로 한다. - inbox 중복 방지는
memberId,newsType,sourceKey기준 unique 정책으로 보장한다. - 언팔로우 시 해당 회원과 크리에이터의 활성 inbox row를 비활성화한다. 재팔로우 시 기존 비활성 row는 복구하지 않는다.
- 이미지 URL은 기존
v2.common.domain.CdnUrlExtensions.toCdnUrl패턴을 따른다. - UTC 문자열 변환은 기존
toUtcIso패턴을 따른다. - 성인 콘텐츠 노출 가능 여부는
MemberContentPreferenceService.canViewAdultContent(member)를 사용한다. - 차단 관계가 있는 크리에이터의 팔로잉 크리에이터, On Air, 스케줄, 최근 소식은 노출하지 않는다.
1. 파일 구조 계획
신규 API 조립 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingController.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt
신규 도메인 조회 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/FollowingNewsType.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowingNewsSourceKey.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingNewsInboxPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInbox.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxJpaRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapter.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowingNewsSourceKeyTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapterTest.kt
기존 파일 수정
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTask.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTaskTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
문서/DDL
- Keep:
docs/20260625_메인_홈_팔로잉_탭_API/prd.md - Keep:
docs/20260625_메인_홈_팔로잉_탭_API/create-home-following-news-inbox-table.sql - Modify:
docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md
Phase 7 후속 보완
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
2. Response data class 초안
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt에는 아래 DTO를 기준으로 추가한다. 필드명은 공개 API 계약이므로 필드 계약을 바꾸는 작업은 먼저 PRD와 이 문서를 갱신한 뒤 별도 변경으로 처리한다.
package kr.co.vividnext.sodalive.v2.api.home.following.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowing
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule
data class HomeFollowingTabResponse(
@JsonProperty("isLoginRequired")
val isLoginRequired: Boolean,
val followingCreators: List<FollowingCreatorResponse>,
val onAirLives: List<FollowingLiveResponse>,
val recentChats: List<ChatRoomListItemResponse>,
val monthlySchedules: List<FollowingScheduleResponse>,
val recentNews: List<FollowingNewsResponse>
) {
companion object {
fun loginRequired(): HomeFollowingTabResponse {
return HomeFollowingTabResponse(
isLoginRequired = true,
followingCreators = emptyList(),
onAirLives = emptyList(),
recentChats = emptyList(),
monthlySchedules = emptyList(),
recentNews = emptyList()
)
}
fun from(home: HomeFollowing): HomeFollowingTabResponse {
return HomeFollowingTabResponse(
isLoginRequired = false,
followingCreators = home.followingCreators.map(FollowingCreatorResponse::from),
onAirLives = home.onAirLives.map(FollowingLiveResponse::from),
recentChats = home.recentChats,
monthlySchedules = home.monthlySchedules.map(FollowingScheduleResponse::from),
recentNews = home.recentNews.map(FollowingNewsResponse::from)
)
}
}
}
data class FollowingCreatorResponse(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImageUrl: String
) {
companion object {
fun from(creator: HomeFollowingCreator): FollowingCreatorResponse {
return FollowingCreatorResponse(
creatorId = creator.creatorId,
creatorNickname = creator.creatorNickname,
creatorProfileImageUrl = creator.creatorProfileImageUrl
)
}
}
}
data class FollowingLiveResponse(
val liveId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val startedAtUtc: String
) {
companion object {
fun from(live: HomeFollowingLive): FollowingLiveResponse {
return FollowingLiveResponse(
liveId = live.liveId,
creatorProfileImageUrl = live.creatorProfileImageUrl,
creatorNickname = live.creatorNickname,
title = live.title,
startedAtUtc = live.startedAtUtc
)
}
}
}
data class FollowingScheduleResponse(
val scheduleId: String,
val creatorId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val type: CreatorActivityType,
val targetId: Long,
val scheduledAtUtc: String,
@JsonProperty("isOnAir")
val isOnAir: Boolean
) {
companion object {
fun from(schedule: HomeFollowingSchedule): FollowingScheduleResponse {
return FollowingScheduleResponse(
scheduleId = schedule.scheduleId,
creatorId = schedule.creatorId,
creatorProfileImageUrl = schedule.creatorProfileImageUrl,
creatorNickname = schedule.creatorNickname,
title = schedule.title,
type = schedule.type,
targetId = schedule.targetId,
scheduledAtUtc = schedule.scheduledAtUtc,
isOnAir = schedule.isOnAir
)
}
}
}
data class FollowingNewsResponse(
val newsId: String,
val type: FollowingNewsType,
val visibleFromAtUtc: String,
val creatorRanking: FollowingCreatorRankingNewsResponse?,
val audioContent: FollowingContentNewsResponse?,
val photoContent: FollowingContentNewsResponse?,
val contentRanking: FollowingContentRankingNewsResponse?,
val communityPost: FollowingCommunityPostNewsResponse?
) {
companion object {
fun from(news: HomeFollowingNews): FollowingNewsResponse {
return FollowingNewsResponse(
newsId = news.newsId,
type = news.type,
visibleFromAtUtc = news.visibleFromAtUtc,
creatorRanking = news.creatorRanking?.toResponse(),
audioContent = news.audioContent?.toContentResponse(),
photoContent = news.photoContent?.toContentResponse(),
contentRanking = news.contentRanking?.toResponse(),
communityPost = news.communityPost?.toResponse()
)
}
}
}
data class FollowingCreatorRankingNewsResponse(
val rank: Int,
val creatorId: Long,
val nickname: String,
val profileImageUrl: String
)
data class FollowingContentNewsResponse(
val contentId: Long,
val contentImageUrl: String?,
val title: String,
val creatorProfileImageUrl: String,
val creatorNickname: String
)
data class FollowingContentRankingNewsResponse(
val rank: Int,
val contentId: Long,
val contentImageUrl: String?,
val title: String
)
data class FollowingCommunityPostNewsResponse(
val postId: Long,
val creatorProfileImage: String,
val creatorNickname: String,
val imageUrl: String?,
val content: String,
val createdAt: String,
val likeCount: Int,
val commentCount: Int
)
private fun HomeFollowingCreatorRankingNews.toResponse() = FollowingCreatorRankingNewsResponse(
rank = rank,
creatorId = creatorId,
nickname = nickname,
profileImageUrl = profileImageUrl
)
private fun HomeFollowingContentNews.toContentResponse() = FollowingContentNewsResponse(
contentId = contentId,
contentImageUrl = contentImageUrl,
title = title,
creatorProfileImageUrl = creatorProfileImageUrl,
creatorNickname = creatorNickname
)
private fun HomeFollowingContentRankingNews.toResponse() = FollowingContentRankingNewsResponse(
rank = rank,
contentId = contentId,
contentImageUrl = contentImageUrl,
title = title
)
private fun HomeFollowingCommunityPostNews.toResponse() = FollowingCommunityPostNewsResponse(
postId = postId,
creatorProfileImage = creatorProfileImage,
creatorNickname = creatorNickname,
imageUrl = imageUrl,
content = content,
createdAt = createdAt,
likeCount = likeCount,
commentCount = commentCount
)
3. Domain / Port 초안
package kr.co.vividnext.sodalive.v2.home.following.domain
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
data class HomeFollowing(
val followingCreators: List<HomeFollowingCreator>,
val onAirLives: List<HomeFollowingLive>,
val recentChats: List<ChatRoomListItemResponse>,
val monthlySchedules: List<HomeFollowingSchedule>,
val recentNews: List<HomeFollowingNews>
)
data class HomeFollowingCreator(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImageUrl: String
)
data class HomeFollowingLive(
val liveId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val startedAtUtc: String
)
data class HomeFollowingSchedule(
val scheduleId: String,
val creatorId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val type: CreatorActivityType,
val targetId: Long,
val scheduledAtUtc: String,
val isOnAir: Boolean
)
data class HomeFollowingNews(
val newsId: String,
val type: FollowingNewsType,
val visibleFromAtUtc: String,
val creatorRanking: HomeFollowingCreatorRankingNews? = null,
val audioContent: HomeFollowingContentNews? = null,
val photoContent: HomeFollowingContentNews? = null,
val contentRanking: HomeFollowingContentRankingNews? = null,
val communityPost: HomeFollowingCommunityPostNews? = null
)
data class HomeFollowingCreatorRankingNews(
val rank: Int,
val creatorId: Long,
val nickname: String,
val profileImageUrl: String
)
data class HomeFollowingContentNews(
val contentId: Long,
val contentImageUrl: String?,
val title: String,
val creatorProfileImageUrl: String,
val creatorNickname: String
)
data class HomeFollowingContentRankingNews(
val rank: Int,
val contentId: Long,
val contentImageUrl: String?,
val title: String
)
data class HomeFollowingCommunityPostNews(
val postId: Long,
val creatorProfileImage: String,
val creatorNickname: String,
val imageUrl: String?,
val content: String,
val createdAt: String,
val likeCount: Int,
val commentCount: Int
)
enum class FollowingNewsType {
CREATOR_RANKING,
CONTENT_RANKING,
COMMUNITY_POST,
AUDIO_CONTENT,
PHOTO_CONTENT
}
package kr.co.vividnext.sodalive.v2.home.following.port.out
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule
import java.time.LocalDateTime
interface HomeFollowingQueryPort {
fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator>
fun findOnAirLives(memberId: Long, canViewAdultContent: Boolean, limit: Int): List<HomeFollowingLive>
fun findMonthlySchedules(memberId: Long, canViewAdultContent: Boolean, now: LocalDateTime, limit: Int): List<HomeFollowingSchedule>
fun findRecentNews(memberId: Long, canViewAdultContent: Boolean, nowUtc: LocalDateTime, limit: Int): List<HomeFollowingNews>
}
interface HomeFollowingNewsInboxPort {
fun insertIgnoreAll(records: List<HomeFollowingNewsInboxRecord>): Int
fun deactivateByMemberIdAndCreatorId(memberId: Long, creatorId: Long): Long
fun findActiveFollowerIds(creatorId: Long): List<Long>
}
data class HomeFollowingNewsInboxRecord(
val memberId: Long,
val creatorId: Long,
val newsType: String,
val sourceKey: String,
val targetId: Long,
val occurredAtUtc: LocalDateTime,
val visibleFromAtUtc: LocalDateTime,
val creatorNickname: String,
val creatorProfileImagePath: String?,
val title: String,
val body: String,
val thumbnailImagePath: String?,
val rank: Int?,
val isAdult: Boolean
)
4. Phase / Task 계획
Phase 1: 응답 DTO, 도메인 모델, Security 기본 골격
-
Task 1.1: 팔로잉 탭 응답 DTO와 domain model 추가
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/FollowingNewsType.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt
- Create:
- RED:
HomeFollowingTabResponse.loginRequired()가isLoginRequired=true와 빈 배열을 반환하는 테스트를 작성한다. - RED:
FollowingNewsResponse변환 결과의 최상위 필드가newsId,type,visibleFromAtUtc와 타입별 nullable nested DTO만 포함하는지 테스트를 작성한다.CREATOR_RANKING은creatorRanking만 non-null이고 다른 nested DTO는 null이어야 한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest"실행, DTO 미구현으로 실패 확인. - GREEN: DTO/domain enum/model을 최소 구현한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: import,
JsonProperty, nullable 필드 정리 후./gradlew --no-daemon ktlintCheck실행.
- Files:
-
Task 1.2: Controller와 Security permitAll 추가
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingController.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt
- Create:
- RED: 비로그인
GET /api/v2/home/following이 200과isLoginRequired=true를 반환하는 MockMvc 테스트를 작성한다. - RED: 로그인 회원 요청이 facade를 호출하고
isLoginRequired=false응답을 반환하는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingControllerTest"실행, endpoint 미구현 또는 security 미설정 실패 확인. - GREEN: controller, facade 빈 골격,
SecurityConfigpermitAll을 최소 구현한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 기존
HomeRecommendationController의@AuthenticationPrincipal패턴과 응답 wrapper 스타일에 맞춘다.
- Files:
Phase 2: 최근 소식 Inbox 저장소
-
Task 2.1: Inbox Entity/JPA repository/DDL 정합성 구현
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInbox.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxJpaRepository.kt - Verify:
docs/20260625_메인_홈_팔로잉_탭_API/create-home-following-news-inbox-table.sql - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapterTest.kt
- Create:
- RED: 같은
memberId/newsType/sourceKey중복 저장이 1건만 유지되어야 하는 테스트를 작성한다. - RED:
memberId/creatorId기준 활성 row 비활성화가 동작하는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest"실행, entity/repository 미구현 실패 확인. - GREEN: Entity와 JPA repository를 DDL 컬럼명에 맞춰 최소 구현한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 컬럼명, enum 저장 방식, timestamp nullable 정책이 DDL과 맞는지 비교한다.
- Files:
-
Task 2.2: Inbox persistence adapter 구현
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingNewsInboxPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapter.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapterTest.kt
- Create:
- RED:
insertIgnoreAll(records)가 중복 source key를 예외 없이 무시하고 신규 row만 저장하는 테스트를 작성한다. - RED:
findActiveFollowerIds(creatorId)가 활성 팔로워만 반환하는 테스트를 작성한다. - 실패 확인: Task 2.1과 같은 단일 테스트 명령 실행, port/adapter 미구현 실패 확인.
- GREEN: JPA
saveAndFlush와 unique 제약 기반DataIntegrityViolationException처리로 중복 source key를 예외 없이 무시하는 idempotent 저장을 최소 구현한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: H2/MySQL dialect 분기 없이 단일 JPA 경로를 유지하고, 동시 적재 시 inserted count는 best-effort임을 검증 기록에 남긴다.
- Files:
Phase 3: 팔로잉 탭 조회 Repository/Service
-
Task 3.1: 팔로잉 크리에이터 조회
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Create:
- RED: 활성 팔로우/활성 크리에이터만 오래된 팔로우순 20개 조회하는
@DataJpaTest(properties = ["spring.cache.type=none"])테스트를 작성한다. - RED: 차단 관계 크리에이터가 제외되는 테스트를 작성한다.
- 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행, repository 미구현 실패 확인. - GREEN:
creator_following,member,block_member조건을 QueryDSL로 최소 구현한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 기본 프로필 이미지와 CDN 변환 책임은 service/facade 중 기존 패턴과 맞는 위치로 정리한다.
- Files:
-
Task 3.2: On Air 조회
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
- RED: 팔로우한 크리에이터의
live_room.is_active=true,channel_name존재 라이브만beginDateTime desc, id desc로 10개 조회하는 테스트를 작성한다. - RED: 성인 콘텐츠 노출 불가이면 19금 라이브가 제외되는 테스트를 작성한다.
- 실패 확인: Task 3.1의 repository 단일 테스트 명령 실행, On Air 미구현 실패 확인.
- GREEN: 기존
DefaultHomeRecommendationQueryRepository.findLiveRecommendations(...)조건을 팔로잉 필터로 확장해 구현한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 라이브 진행 중 판단 조건이 스케줄
isOnAir와 중복되면 private helper로 추출한다.
- Files:
-
Task 3.3: 이달의 스케줄 조회
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
- RED: KST 오늘 00:00 이상 다음 달 00:00 미만의 라이브/오디오 일정을
scheduledAt asc로 3개 조회하는 테스트를 작성한다. - RED: 오늘 이전 일정과 차단 크리에이터 일정이 제외되는 테스트를 작성한다.
- 실패 확인: repository 단일 테스트 명령 실행, schedule 미구현 실패 확인.
- GREEN: 기존
CreatorChannelHomeQueryRepository.findSchedules(...)의 live/audio 조건을 팔로잉 전체 조회로 확장한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR:
scheduleId는{TYPE}:{targetId}형식으로 안정적으로 생성한다.
- Files:
-
Task 3.4: 최근 소식 조회
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
- RED:
memberId,isActive=true,visibleFromAtUtc <= nowUtc조건으로visibleFromAtUtc desc, id desc30개를 조회하는 테스트를 작성한다. - RED: 최근 소식 domain이 타입별 nested DTO를 채우고,
COMMUNITY_POST/AUDIO_CONTENTtarget 비활성 필터를 유지하는 테스트를 작성한다. - 실패 확인: repository 단일 테스트 명령 실행, recent news 미구현 실패 확인.
- GREEN: inbox table 조회와
HomeFollowingNews변환을 최소 구현한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 조회 시 차단/성인/target 활성 조건을 과도하게 조인하지 않도록 필요한 조건만 유지한다.
- Files:
-
Task 3.5: HomeFollowingQueryService 조립
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt
- Create:
- RED: query service가 팔로잉 크리에이터 20, On Air 10, 스케줄 3, 최근 소식 30 limit로 port를 호출하는 테스트를 작성한다.
- RED:
MemberContentPreferenceService.canViewAdultContent(member)결과가 조회 port에 전달되는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryServiceTest"실행, service 미구현 실패 확인. - GREEN: service에서 now/limit/성인 노출 정책을 조립한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR:
nowProvider: () -> LocalDateTime을 주입해 테스트 시간을 고정한다.
- Files:
-
Task 3.6: Inbox 중복 insert 충돌 통합 테스트 보강
- Files:
- Modify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapterTest.kt
- Modify:
- RED: 실제
HomeFollowingNewsInboxJpaRepository로 동일memberId/newsType/sourceKeyunique 충돌을 발생시킨 뒤, 같은 테스트 흐름에서insertIgnoreAll(records)또는 repository 조회가 예외 없이 동작하는 통합 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest"실행, 실제 DB 충돌 후 persistence context/transaction 상태 검증 실패를 확인한다. - GREEN: 필요 시 adapter의 중복 충돌 처리에서 persistence context 정리 또는 트랜잭션 경계를 최소 보강한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: mock 기반 race 테스트와 통합 테스트의 책임을 분리해, mock은 분기 검증만 하고 통합 테스트는 실제 Hibernate 세션/트랜잭션 유효성을 검증하도록 정리한다.
- Files:
-
Task 3.7: 라이브 성별·크리에이터 입장 제한 회귀 수정
Goal 실행
P3-R1:REV-P3-001에 따라 On Air와 라이브 스케줄에서 기존 라이브 입장 제한을 동일하게 적용한다.-
시작 조건:
reviews/phase-3-review.md의REV-P3-001확정, 기존 Task 3.1~3.6 완료. -
완료 증거: 제한 불일치 재현 테스트의 RED 확인, 최소 구현 후 focused test와 E2E 통과, 검증 기록 누적.
-
범위 밖: 공개 응답 스키마 변경, 오디오 스케줄 정책 변경, 라이브 입장 정책 자체의 재정의.
-
Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingQueryPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingQueryServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
- Modify:
-
RED: 회원 본인인증 성별을 우선한 effective gender와 크리에이터 회원 여부가 query port에 전달되는 테스트를 추가하고 실패를 확인한다.
-
RED: 성별 제한이 맞지 않는 라이브와
isAvailableJoinCreator=false인 타 크리에이터 라이브가 On Air 및 라이브 스케줄에서 제외되는 repository 테스트를 추가하고 실패를 확인한다. -
GREEN: 기존
LiveRoomQueryRepositoryImpl/DefaultCreatorChannelHomeQueryRepository의genderRestriction및 크리에이터 입장 제한 조건을 재사용 가능한 최소 QueryDSL 조건으로 적용한다. -
GREEN 확인: 아래 focused test를 실행해 신규 회귀와 기존 섹션 조회가 통과하는지 확인한다.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryServiceTest" ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest" -
REFACTOR: live 조건만 정리하고 공개 DTO와 최근 소식 로직은 변경하지 않는다.
-
Phase 3 리뷰 회귀 Gate
Goal 실행 P3-R-GATE: Phase 3의 라이브 입장 정책 수정과 팔로잉 탭 조립 회귀를 최종 판정한다.
- 시작 조건:
P3-R1완료. - 완료 증거: 아래 명령이 모두
BUILD SUCCESSFUL이고 결과가## 6. 검증 기록과reviews/phase-3-review.md에 누적됨. - 범위 밖: 전체 회귀 실패와 무관한 코드 수정, 테스트 삭제·완화.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryServiceTest"
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"
./gradlew --no-daemon ktlintCheck
Phase 4: 최근 소식 Publish Service와 기존 이벤트 연결
-
Task 4.1: sourceKey 생성 정책 구현
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowingNewsSourceKey.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowingNewsSourceKeyTest.kt
- Create:
- RED:
CREATOR_RANKING:{creatorId}:{aggregationStartAtUtc}형식 source key 생성 테스트를 작성한다. - RED:
AUDIO_CONTENT:{contentId}와COMMUNITY_POST:{postId}source key 생성 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNewsSourceKeyTest"실행, source key 미구현 실패 확인. - GREEN: source key 생성 object를 최소 구현한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 문자열 상수는
FollowingNewsTypeenum 이름과 불일치하지 않게 정리한다.
- Files:
-
Task 4.2: HomeFollowingNewsPublishService 구현
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishServiceTest.kt
- Create:
- RED:
publishFreeCommunityPostCreated(...)가 무료 커뮤니티 게시글에 대해서만 현재 active follower에게 inbox record를 생성하고, 유료 커뮤니티 게시글 생성 흐름은 publish service를 호출하지 않는 테스트를 작성한다. - RED:
publishContentUploaded(...)가visibleFromAtUtc를 콘텐츠 공개 시각으로 저장하는 테스트를 작성한다. - RED:
publishCreatorRankingVisible(...)이rank와 랭킹 스냅샷visibleFromAtUtc를 저장하는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"실행, service 미구현 실패 확인. - GREEN: publish service에서 follower 조회, record 변환,
insertIgnoreAll호출을 최소 구현한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 외부 MQ/outbox 없이 동작하되 호출부가 service 메서드에만 의존하도록 public API를 작게 유지한다.
- Files:
-
Task 4.3: 언팔로우 시 inbox 비활성화 연동
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt
- Modify:
- RED: 언팔로우 시 해당
memberId/creatorId의 active inbox row가isActive=false가 되는 테스트를 작성한다. - RED: 재팔로우 시 기존 비활성 row가 복구되지 않는 테스트를 작성한다.
- 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"실행, inbox 비활성화 미연동 실패 확인. - GREEN: 기존 언팔로우 처리 성공 후
HomeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(...)를 호출한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 팔로잉 공개 API 스키마는 변경하지 않는다.
- Files:
-
Task 4.4: 크리에이터 랭킹 소식 발행 연결
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt
- Modify:
- RED:
refreshLastCompletedWeek(...)가 스냅샷 저장 성공 후publishCreatorRankingVisible(...)을visibleFromAtUtc,rank,creatorId로 호출하는 테스트를 작성한다. - RED:
snapshotPort.replaceSnapshots(...)실패 시publishCreatorRankingVisible(...)이 호출되지 않는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest"실행, publish 미연동 실패 확인. - GREEN:
snapshotPort.replaceSnapshots(...)성공 직후snapshots.mapIndexed { index, snapshot -> rank = index + 1 }로 publish service를 호출한다. - 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 월요일 01:00 생성, 09:00 노출 정책은 inbox
visibleFromAtUtc로만 처리한다.
- Files:
-
Task 4.5: 콘텐츠/커뮤니티 업로드 소식 발행 연결
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
- Modify:
- RED:
CreatorCommunityService.createCommunityPost(...)성공 후 무료 게시글이면publishFreeCommunityPostCreated(...)가 post id, creator id, 본문, 생성 시각으로 호출되고, 유료 게시글이면 호출되지 않는 테스트를 작성한다. - RED:
AudioContentService.createAudioContent(...)에서releaseDate <= now인 즉시 공개 콘텐츠 저장 성공 후publishContentUploaded(...)가 호출되는 테스트를 작성한다. - RED:
AudioContentService.createAudioContent(...)에서releaseDate > now인 예약 공개 콘텐츠 생성 시점에는publishContentUploaded(...)가 호출되지 않는 테스트를 작성한다. - RED:
AudioContentService.releaseContent()가 예약 콘텐츠를 active로 바꾸는 시점에publishContentUploaded(...)를 호출하는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.content.AudioContentServiceTest"./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest"- 기대 결과: publish 미연동으로 FAIL
- GREEN:
AudioContentService.createAudioContent(...),AudioContentService.releaseContent(),CreatorCommunityService.createCommunityPost(...)의 트랜잭션 성공 경로에서 publish service를 호출한다. - 통과 확인: 위 두 단일 테스트 명령 재실행, PASS 확인.
- REFACTOR: 결제/수정/관리자 저장 중 실제 공개 이벤트가 아닌 경로에서 중복 발행하지 않도록 sourceKey unique와 호출 지점을 함께 점검한다.
- Files:
-
Task 4.6: 언팔로우·재팔로우와 최근 소식 발행 동시성 보장
Goal 실행
P4-R1:REV-P4-001에 따라 follower 판정부터 inbox insert까지의 경계를 팔로우 상태 변경과 직렬화해, 언팔로우 이전 이벤트가 재팔로우 후 새 소식으로 노출되지 않게 한다.-
시작 조건:
reviews/phase-4-review.md의REV-P4-001확정, 기존 Task 4.1~4.5 완료. -
완료 증거: 두 트랜잭션을 제어한 회귀 테스트의 RED 확인, 최소 구현 후 focused test 통과, 실제 결과를
## 6. 검증 기록과reviews/phase-4-review.md에 누적. -
범위 밖: 외부 MQ/outbox/worker 도입, inbox 공개 API·DDL 변경, 팔로우 알림 정책 변경, 관련 없는 publish 경로 리팩터링.
-
Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/port/out/HomeFollowingNewsInboxPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxJpaRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapter.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/HomeFollowingNewsInboxPersistenceAdapterTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
- Modify:
-
RED: publish가 active follower를 읽은 뒤 insert하기 전에 같은 회원이 언팔로우하는 순서를 두 트랜잭션과 latch로 고정한다. 언팔로우 완료 후 publish insert가 늦게 완료되고 다시 팔로우하면 언팔로우 이전 이벤트가 조회되는 현재 실패를 재현한다.
-
GREEN: follower 판정과 inbox insert를 하나의 트랜잭션 경계로 묶고 해당
creator_followingrow를 팔로우/언팔로우 상태 변경과 동일한 잠금 순서로 직렬화한다. publish가 먼저 완료되면 뒤이은 언팔로우가 새 row까지 비활성화하고, 언팔로우가 먼저 완료되면 publish가 해당 회원 row를 생성하지 않아야 한다. -
GREEN: 재팔로우는 기존 비활성 inbox를 복구하지 않으며, 재팔로우 이후 발생한 새 이벤트만 active row로 생성되는 기존 정책을 유지한다.
-
GREEN 확인: 아래 focused test를 실행해 동시성 회귀와 기존 중복 방지·발행·언팔로우 테스트가 모두 통과하는지 확인한다.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest" --tests "kr.co.vividnext.sodalive.member.MemberServiceTest" -
REFACTOR: H2/MySQL에서 검증 가능한 기존 JPA 경로와 공개 port 범위를 유지하고, 동시성 보장에 필요하지 않은 계층·설정·DDL을 추가하지 않는다.
-
Phase 4 Review Gate
Goal 실행 P4-R-GATE: P4-R1의 동시성 보장과 기존 최근 소식 발행·조회 계약을 최종 판정한다.
- 시작 조건:
P4-R1완료. - 완료 증거: 아래 명령이 모두
BUILD SUCCESSFUL이고 결과가## 6. 검증 기록과reviews/phase-4-review.md에 누적됨. - 범위 밖: 테스트 삭제·완화, 공개 API·DDL 확장, 이번 회귀와 무관한 코드 수정.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest" --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"
./gradlew --no-daemon ktlintCheck
Expected: publish와 언팔로우의 완료 순서와 무관하게 최종 언팔로우 상태에서는 해당 creator의 active inbox가 없고, 재팔로우 후에는 재팔로우 이후 이벤트만 노출된다.
-
전체
./gradlew test는 follower fan-out과 팔로우 상태 변경 경계만 보완하는 국소 수정이므로 기본 Gate에서 생략한다. focused test 또는 E2E가 공유 트랜잭션 경계의 회귀를 충분히 판정하지 못하면 전체 회귀로 확장하고 근거와 결과를 기록한다. -
Task 4.7: 통합 팔로우 API의
isActive=false경로에서 inbox 비활성화Goal 실행
P4-R2:REV-P4-002에 따라creatorFollow(..., isActive=false)가 새 active 팔로우를 만들지 않고, 기존 관계의 active inbox를 비활성화해 재팔로우 후 과거 소식이 다시 노출되지 않게 한다.-
시작 조건:
reviews/phase-4-review.md의REV-P4-002확정,P4-R1과P4-R-GATE완료. -
완료 증거: 통합 팔로우 API 경로의 회귀 테스트 RED 확인, 최소 구현 후
MemberServiceTest통과, Phase 4 회귀 Gate와 검증 기록 누적. -
범위 밖:
CreatorFollowRequest·controller 공개 스키마 변경, 알림 설정 정책 변경, 외부 MQ/outbox 도입, 전용creatorUnFollow(...)경로 리팩터링. -
Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowRequest.kt
- Modify:
-
Interfaces:
- Consumes:
MemberService.creatorFollow(creatorId: Long, isNotify: Boolean, isActive: Boolean, memberId: Long),HomeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(memberId: Long, creatorId: Long). - Produces: 기존 public method·request 계약을 바꾸지 않고
isActive=false일 때 전용 언팔로우 경로와 같은 inbox 최종 상태.
- Consumes:
-
RED: 관계가 없는 회원이
creatorFollow(..., isActive=false)를 호출해도 새 active 팔로우가 생성되지 않는지 검증한다. -
RED: active 팔로우와 active inbox를 준비한 뒤
creatorFollow(..., isActive=false)를 호출하고 관계와 inbox가 모두 inactive인지 검증한다. 이어creatorFollow(..., isActive=true)를 호출해 기존 inbox가 inactive로 유지되는지 검증한다. -
RED 확인: 아래 단일 테스트를 실행해
creatorFollow(..., isActive=false)직후 inbox가 여전히 active인 assertion 실패를 확인한다../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.member.MemberServiceTest" -
GREEN: 관계가 없고
isActive=false면 전용creatorUnFollow(...)와 동일하게 새 active 관계를 만들지 않는다. 기존creator_followingrow에isActive=false를 반영하는 같은 트랜잭션에서는homeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(...)를 호출한다.isActive=true인 알림 변경·재팔로우 경로에서는 기존 비활성 inbox를 복구하지 않는다. -
GREEN 확인: 같은
MemberServiceTest명령을 재실행해 전용 언팔로우와 통합 팔로우 API 경로의 회귀가 모두 통과하는지 확인한다. -
REFACTOR: 두 공개 method의 계약과 비관적 잠금 순서를 유지하고, 공통화가 한 번만 쓰이는 추상화나 신규 계층은 추가하지 않는다.
-
Phase 4 3차 리뷰 회귀 Gate
Goal 실행 P4-R2-GATE: P4-R2의 통합 언팔로우 경로와 기존 publish·언팔로우 동시성 계약을 최종 판정한다.
- 시작 조건:
P4-R2완료. - 완료 증거: 아래 명령이 모두
BUILD SUCCESSFUL이고 결과가## 6. 검증 기록과reviews/phase-4-review.md에 누적됨. - 범위 밖: 테스트 삭제·완화, 공개 API·DDL 변경, 이번 회귀와 무관한 코드 수정.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"
./gradlew --no-daemon ktlintCheck
Expected: 전용 언팔로우와 creatorFollow(..., isActive=false) 어느 경로를 사용해도 새 active 팔로우가 생기지 않고 기존 active inbox가 비활성화되며, 재팔로우는 과거 inbox를 복구하지 않는다.
Phase 5: Facade 통합, 최근 대화 재사용, API End-to-End
-
Task 5.1: HomeFollowingFacade 통합
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacade.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt
- Modify:
- RED:
member == null이면 query/chat 서비스를 호출하지 않고HomeFollowingTabResponse.loginRequired()를 반환하는 테스트를 작성한다. - RED: 로그인 회원이면 query service와
ChatRoomListService.getRooms(member, "ALL", null, 10)를 호출해 응답을 조립하는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacadeTest"실행, facade 미구현 실패 확인. - GREEN: facade 조립 로직을 최소 구현한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 한 섹션 데이터 부족은 빈 배열/가능한 개수로 성공 처리한다.
- Files:
-
Task 5.2: End-to-End API 통합 테스트
- Files:
- Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
- Test:
- RED: 비로그인 호출이 200,
isLoginRequired=true, 모든 배열 빈 값인지 검증하는 통합 테스트를 작성한다. - RED: 로그인 회원 호출이 팔로잉 크리에이터/On Air/최근 대화/스케줄/최근 소식을 모두 조립하는 통합 테스트를 작성한다.
- RED:
FollowingNewsResponse최상위에creatorProfileImageUrl,creatorNickname,title,body,thumbnailImageUrl,targetId,occurredAtUtc,rank가 없고, 타입과 일치하는 nested DTO만 non-null인지 JSON path 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행, 통합 미구현 실패 확인. - GREEN: 누락된 wiring, bean 등록, security 설정을 최소 수정한다.
- 통과 확인: 같은 단일 테스트 명령 실행, PASS 확인.
- REFACTOR: 테스트 데이터 builder가 과하게 커지면 테스트 내부 private helper로만 분리한다.
- Files:
Phase 5.5: 최근 소식 응답 계약 nested DTO 변경 후속 구현
이 Phase는 Phase 1-5 구현 완료 후 변경된 공개 응답 계약을 실제 코드에 반영하기 위한 후속 작업이다. 기존 flat
FollowingNewsResponse를 제거하고, 타입별 nullable nested DTO 계약과 무료 커뮤니티 게시글 전용 발행 계약을 구현한다.
작업 의존성
Task 5.5.1DTO/domain 계약 변경과Task 5.5.3무료 커뮤니티 발행 제한은 서로 독립적으로 시작할 수 있다.Task 5.5.2repository enrichment는Task 5.5.1의 domain model 변경 이후 진행한다.Task 5.5.4E2E 검증은Task 5.5.1,Task 5.5.2,Task 5.5.3완료 후 진행한다.Task 5.5.5회귀 검증은 모든 구현 task 완료 후 진행한다.
-
Task 5.5.1: FollowingNewsResponse / HomeFollowingNews nested DTO 모델 전환
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt
- Modify:
- RED:
FollowingNewsResponse최상위 JSON에newsId,type,visibleFromAtUtc,creatorRanking,contentRanking,audioContent,photoContent,communityPost만 존재하는 테스트를 작성한다. - RED: 최상위 JSON에 기존 flat 필드인
creatorProfileImageUrl,creatorNickname,title,body,thumbnailImageUrl,targetId,occurredAtUtc,rank가 존재하지 않는 테스트를 작성한다. - RED:
CREATOR_RANKING이면creatorRanking만 non-null이고rank,creatorId,nickname,profileImageUrl을 포함하는 테스트를 작성한다. - RED:
CONTENT_RANKING이면contentRanking만 non-null이고rank,contentId,contentImageUrl,title을 포함하는 테스트를 작성한다. - RED:
AUDIO_CONTENT이면audioContent만 non-null이고contentId,contentImageUrl,title,creatorProfileImageUrl,creatorNickname을 포함하며releaseDate가 없음을 검증하는 테스트를 작성한다. - RED:
PHOTO_CONTENT이면photoContent만 non-null이고contentId,contentImageUrl,title,creatorProfileImageUrl,creatorNickname을 포함하며releaseDate가 없음을 검증하는 테스트를 작성한다. - RED:
COMMUNITY_POST이면communityPost만 non-null이고postId,creatorProfileImage,creatorNickname, nullableimageUrl,content, UTCcreatedAt,likeCount,commentCount를 포함하는 테스트를 작성한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest"실행, 기존 flat DTO/domain 구조로 실패 확인. - GREEN:
HomeFollowingNews에 공통 필드newsId,type,visibleFromAtUtc와 타입별 nullable domain payload를 추가한다. - GREEN:
FollowingNewsResponse에 공통 필드와 타입별 nullable response payload를 추가하고, domain payload를 response payload로 1:1 변환한다. - REFACTOR: 타입별 DTO 변환 helper는
HomeFollowingTabResponse.kt내부 private function 또는 companion object로만 유지하고, sealed class/상속 구조는 도입하지 않는다. - 통과 확인: 위 단일 테스트 명령 재실행, PASS 확인.
- Files:
-
Task 5.5.2: 최근 소식 조회 repository 타입별 nested payload enrichment 구현
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
- RED:
CREATOR_RANKINGrow가creatorRanking.rank,creatorRanking.creatorId,creatorRanking.nickname,creatorRanking.profileImageUrl을 채우고 다른 nested payload는 null인 테스트를 작성한다. - RED:
AUDIO_CONTENTrow가AudioContent원천에서contentId,contentImageUrl,title, creator profile/nickname을 채우고 공개 시각은 최상위visibleFromAtUtc를 사용하는 테스트를 작성한다. - RED:
COMMUNITY_POSTrow가 무료CreatorCommunity원천에서postId,creatorProfileImage,creatorNickname, CDNimageUrl, 전체content, UTCcreatedAt, activelikeCount, active top-levelcommentCount를 채우는 테스트를 작성한다. - RED:
COMMUNITY_POST원천 게시글이price > 0이면 inbox row가 있더라도 최근 소식에서 제외되는 테스트를 작성한다. - RED: 기존
AUDIO_CONTENT,COMMUNITY_POST원천 target 비활성 제외 테스트가 nested domain 구조에서도 유지되도록 수정한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행, flatHomeFollowingNews매핑과 enrichment 미구현으로 실패 확인. - GREEN: 기존 inbox 후보 조회는 유지하되, 조회된
targetId를 타입별로 모아 batch 조회한다. - GREEN:
AUDIO_CONTENT는audio_content원천에서 active row만 조회하고 이미지 URL을 변환해audioContentpayload를 만든다. - GREEN:
COMMUNITY_POST는 active이고price <= 0인 원천 게시글만 조회하며, active like count와 active top-level comment count를 postIds 기준으로 batch 집계해communityPostpayload를 만든다. - GREEN:
CREATOR_RANKING은 inbox/creator 정보와rank_no로creatorRankingpayload를 만든다. - GREEN: 아직 생성하지 않는
CONTENT_RANKING,PHOTO_CONTENT는 domain/DTO 타입만 유지하고, 원천 조회 경로가 없으면 API에 노출하지 않는다. - REFACTOR: enrichment helper는
DefaultHomeFollowingQueryRepository.ktprivate method로 둔다. 새 port/repository interface는 이 task에서 만들지 않는다. - 통과 확인: 위 단일 테스트 명령 재실행, PASS 확인.
- Files:
-
Task 5.5.3: 무료 커뮤니티 게시글만 COMMUNITY_POST 최근 소식 발행
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/application/HomeFollowingNewsPublishServiceTest.kt
- Modify:
- RED: 무료 커뮤니티 게시글 생성 성공 후
HomeFollowingNewsPublishService가 post id, creator id, creator nickname/profile, 본문, 이미지 path, 생성 시각으로 호출되는 테스트를 작성한다. - RED: 유료 커뮤니티 게시글 생성 성공 후
HomeFollowingNewsPublishService가 호출되지 않는 테스트를 작성한다. - RED: 무료 커뮤니티 게시글 최근 소식 발행 실패가 원 게시글 생성 성공을 실패로 전파하지 않는 기존 after-commit 격리 테스트를 유지한다.
- RED:
HomeFollowingNewsPublishServiceTest의 커뮤니티 발행 테스트명/설명을 무료 게시글 발행 계약에 맞게 갱신한다. - 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"실행, 유료 게시글도 발행하는 기존 구현으로 실패 확인. - GREEN:
CreatorCommunityService에서post.price <= 0인 경우에만 최근 소식 publish를 예약한다. - GREEN: publish service 메서드명은 구현 시 선택한다. 이름을
publishFreeCommunityPostCreated(...)로 바꾸면 모든 호출부/테스트를 함께 갱신하고, 기존 이름을 유지하면 호출 조건으로 무료 게시글 전용 계약을 보장한다. - REFACTOR: 유료 게시글 미리보기 마스킹 로직이 최근 소식 발행에서 더 이상 쓰이지 않으면 제거하되, 커뮤니티 탭/상세의 유료 콘텐츠 정책은 건드리지 않는다.
- 통과 확인: 위 테스트 명령 재실행, PASS 확인.
- Files:
-
Task 5.5.4: 팔로잉 탭 API E2E nested response 계약 검증
- Files:
- Modify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
- Modify:
- RED: 로그인 회원 API 응답에
CREATOR_RANKING,AUDIO_CONTENT, 무료COMMUNITY_POST최근 소식 fixture를 포함한다. - RED: 각 recentNews item의 최상위 flat 필드
creatorProfileImageUrl,creatorNickname,title,body,thumbnailImageUrl,targetId,occurredAtUtc,rank가 존재하지 않는 JSON path 테스트를 작성한다. - RED:
CREATOR_RANKINGitem은creatorRanking만 non-null이고rank,creatorId,nickname,profileImageUrl을 포함하는지 검증한다. - RED:
AUDIO_CONTENTitem은audioContent만 non-null이고releaseDate가 없으며 최상위visibleFromAtUtc가 존재하는지 검증한다. - RED:
COMMUNITY_POSTitem은communityPost만 non-null이고postId,creatorProfileImage,creatorNickname,imageUrl,content, UTCcreatedAt,likeCount,commentCount를 포함하는지 검증한다. - RED: 유료 커뮤니티 게시글은 최근 소식 응답에 노출되지 않는지 검증한다.
- 실패 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행, 기존 flat API 응답으로 실패 확인. - GREEN: Task 5.5.1-5.5.3 구현으로 대부분 통과해야 한다. wiring 누락이 있으면 관련 production file만 최소 수정한다.
- REFACTOR: E2E fixture helper는 테스트 파일 내부 private helper로만 분리한다.
- 통과 확인: 위 단일 테스트 명령 재실행, PASS 확인.
- Files:
-
Task 5.5.5: Phase 5.5 문서/회귀 검증 기록
- Files:
- Verify:
docs/20260625_메인_홈_팔로잉_탭_API/prd.md - Modify:
docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md - Verify:
docs/20260629_커뮤니티_게시글_좋아요_상태/prd.md - Verify: Phase 5.5에서 변경한 Kotlin source/test
- Verify:
- TDD 예외 사유: 문서/회귀 검증 작업이며 신규 production behavior를 만들지 않는다.
- 대체 검증 방법:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest"./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"./gradlew --no-daemon test./gradlew --no-daemon ktlintCheck
- 문서 검증 grep:
rg -n "creatorProfileImageUrl|creatorNickname|thumbnailImageUrl|targetId|occurredAtUtc|rank: Int\?|publishCommunityPostCreated|유료 커뮤니티 최근 소식 미리보기" docs/20260625_메인_홈_팔로잉_탭_API- 검색 결과가 팔로잉 크리에이터/스케줄/타입별 nested DTO/내부 inbox 컬럼/과거 검증 기록 맥락인지 확인하고, stale flat
FollowingNewsResponse공개 계약이면 수정한다.
- 검증 결과 기록: Phase 5.5 완료 시 실행 명령, 결과, 실패 시 원인과 후속 조치를 이 문서의
## 6. 검증 기록에 한국어로 누적 기록한다.
- Files:
Phase 6: 문서/회귀 검증
-
Task 6.1: 문서 동기화 확인
- Files:
- Verify:
docs/20260625_메인_홈_팔로잉_탭_API/prd.md - Verify:
docs/20260625_메인_홈_팔로잉_탭_API/create-home-following-news-inbox-table.sql - Modify:
docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md
- Verify:
- TDD 예외 사유: 문서 검증 작업이며 실행 코드가 없다.
- 대체 검증 방법:
rg -n "creatorProfileImageUrl|creatorNickname|thumbnailImageUrl|targetId|occurredAtUtc|rank: Int\\?|publishCommunityPostCreated|유료 커뮤니티 최근 소식 미리보기" docs/20260625_메인_홈_팔로잉_탭_API로 최근 소식 flat 공개 응답 필드와 유료 커뮤니티 발행 계약이 남아 있는지 확인한다. 단, 팔로잉 크리에이터/스케줄/타입별 nested DTO/내부 inbox 컬럼 맥락은 허용한다. - 실행 명령:
./gradlew tasks --all - 기대 결과:
BUILD SUCCESSFUL
- Files:
-
Task 6.2: 전체 회귀 검증
- Files:
- Verify: 전체 Kotlin source/test
- TDD 예외 사유: 전체 회귀 검증 task이며 신규 테스트 작성 대상이 아니다.
- 대체 검증 방법:
./gradlew --no-daemon test./gradlew --no-daemon ktlintCheck
- 기대 결과: 두 명령 모두
BUILD SUCCESSFUL - 검증 결과 기록: 각 task 완료 시 실행 명령, 결과, 실패 시 원인과 후속 조치를 이 문서의 해당 task 아래에 한국어로 누적 기록한다.
- Files:
-
Task 6.3:
newsId공개 계약을 현재 구현과 동기화Goal 실행
P6-R1:REV-P6-001에 따라newsId가home_following_news_inbox.id의 10진 문자열임을 PRD에 명시하고,scheduleId의{TYPE}:{targetId}계약과 분리한다.-
시작 조건:
reviews/phase-6-review.md의REV-P6-001확정, 현재 repository/test/E2E의newsId동작 확인. -
완료 증거: PRD 식별자 정책과 최근 소식 정렬·동률 해소 설명이 현재 구현과 일치하고, 대체 검증 결과를
## 6. 검증 기록과reviews/phase-6-review.md에 누적. -
범위 밖: 공개 응답 필드 추가·삭제, runtime 코드·테스트·DDL 변경, 기존
newsId값 형식 변경. -
Files:
- Modify:
docs/20260625_메인_홈_팔로잉_탭_API/prd.md - Modify:
docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
- Modify:
-
TDD 예외 사유: 현재 공개 동작을 변경하지 않는 문서 계약 정합성 보완이며 신규 production behavior가 없다.
-
prd.md의 공통 식별자 설명에서scheduleId만{TYPE}:{targetId}형식으로 유지하고,newsId는 inbox PK의 10진 문자열이며 같은 노출 시각의 정렬·CONTENT_RANKING동률 해소에 사용한다고 명시한다. -
아래 검색으로 PRD, repository, repository test, E2E의
newsId형식을 정적으로 대조한다.rg -n "newsId|scheduleId|home_following_news_inbox\\.id" docs/20260625_메인_홈_팔로잉_탭_API/prd.md src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt git diff --check -
문서 명령 유효성만 확인하는
./gradlew tasks --all을 실행해BUILD SUCCESSFUL을 확인한다. 컴파일과 테스트는 실행하지 않는다.
-
Phase 6 Review Gate
Goal 실행 P6-R-GATE: P6-R1의 식별자 계약 정합성과 공개 동작 무변경을 최종 판정한다.
- 시작 조건:
P6-R1완료. - 완료 증거: 정적 검색과
git diff --check,./gradlew tasks --all결과가## 6. 검증 기록과reviews/phase-6-review.md에 누적됨. - 범위 밖: 컴파일·테스트 재실행, runtime 코드·테스트·DDL 변경.
Expected: scheduleId는 {TYPE}:{targetId}, newsId는 inbox PK 10진 문자열로 문서와 현재 구현이 일치하며 API 응답 동작은 변경되지 않는다.
-
Task 6.4: 크리에이터 랭킹 최신 공개 배치 기준 문서 동기화
Goal 실행
P6-R2:REV-P6-002에 따라 PRD의 최신CREATOR_RANKING배치 판정 기준을 현재 구현의WEEKLY,DONEjob 우선·legacy snapshot 제한 fallback 정책과 일치시킨다.-
시작 조건:
reviews/phase-6-review.md의REV-P6-002확정,P7-R1과P7-R-GATE완료. -
완료 증거: PRD Feature F와 기술 제약의 최신 배치 설명이 Task 7.2 및 repository 조건과 일치하고, 정적 검색 결과를
## 6. 검증 기록과reviews/phase-6-review.md에 누적. -
범위 밖: runtime 조회 조건·테스트·DDL·공개 응답 변경, 랭킹 집계·점수·공개 시각 정책 변경.
-
Files:
- Modify:
docs/20260625_메인_홈_팔로잉_탭_API/prd.md - Modify:
docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
-
TDD 예외 사유: 현재 공개 동작을 바꾸지 않는 문서 정합성 보완이며 신규 production behavior가 없다.
-
prd.md의 두 최신 공개 배치 설명을 적용 가능한creator_ranking_snapshot_job의 최신WEEKLY,DONE,visibleFromAtUtc <= nowUtc시각 우선으로 수정한다. -
적용 가능한
DONEjob이 전혀 없는 legacy/backfill 데이터에서만 최신 공개 snapshot 시각을 fallback으로 사용하고, 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다고 명시한다. -
아래 검색으로 PRD, Task 7.2, repository, 빈 최신 배치 회귀 테스트를 정적으로 대조한다.
rg -n "creator_ranking_snapshot_job|creator_ranking_snapshot|최신 공개 배치|WEEKLY|DONE|legacy|fallback" docs/20260625_메인_홈_팔로잉_탭_API/prd.md docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt git diff --check -
문서 명령 유효성만 확인하는
./gradlew tasks --all을 실행해BUILD SUCCESSFUL을 확인한다. 컴파일과 테스트는 실행하지 않는다.
-
Phase 6 3차 리뷰 회귀 Gate
Goal 실행 P6-R2-GATE: P6-R2의 최신 공개 배치 문서 정합성과 runtime 무변경을 최종 판정한다.
- 시작 조건:
P6-R2완료. - 완료 증거: 정적 검색과
git diff --check,./gradlew tasks --all결과가## 6. 검증 기록과reviews/phase-6-review.md에 누적됨. - 범위 밖: 컴파일·테스트 실행, runtime 코드·테스트·DDL 변경.
Expected: PRD가 최신 완료 job의 빈 배치를 포함한 현재 판정과 일치하고, 적용 가능한 DONE job이 없는 경우에만 snapshot fallback을 허용한다.
Phase 7: 최근 소식 랭킹 조회 정책 보완
Phase 결과: 최근 소식에서 크리에이터 랭킹은 최신 공개 배치만, 콘텐츠 랭킹은 동일 콘텐츠의 최신 소식 하나만 표시된다.
선행조건: Phase 1~6 완료와 2026-07-30 확정 요구사항 반영.
Phase 완료 조건: P7-T1과 P7-GATE 완료, focused test·직접 영향 회귀·문서 검증 기록 누적.
-
Task 7.1: 최신 크리에이터 랭킹 배치 필터와 콘텐츠 랭킹 중복 제거
Goal 실행
P7-T1:findRecentNews(...)가 랭킹 정책을 최대 30개 제한 전에 적용해 최신 크리에이터 배치와 콘텐츠별 최신 랭킹 소식만 반환한다.-
시작 조건: PRD Feature F와 Decision Log의 2026-07-30 결정 확인.
-
완료 증거: 아래 TDD 체크박스 전체 완료, focused test 통과, 실제 결과를
## 6. 검증 기록에 누적. -
범위 밖:
CONTENT_RANKINGinbox 발행, 콘텐츠 랭킹 스냅샷 연동, 공개 API 스키마·DDL 변경, 다른 최근 소식 타입 리팩터링. -
Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
-
Interfaces:
- Consumes:
creator_ranking_snapshot.ranking_type,visible_from_at,home_following_news_inbox.news_type,target_id,visible_from_at_utc,id. - Produces: 기존
HomeFollowingQueryPort.findRecentNews(memberId, canViewAdultContent, nowUtc, limit)계약을 변경하지 않은 필터링 결과.
- Consumes:
-
RED:
shouldFindOnlyLatestVisibleCreatorRankingBatchInRecentNews테스트에 직전·최신 공개 크리에이터 랭킹 스냅샷과 inbox를 저장한다. 신규 배치 공개 전에는 직전 배치가 조회되고, 공개 후에는 최신 배치만 조회되며, 최신 배치에 없는 크리에이터의 과거 순위는 제외되는지 검증한다. -
RED:
shouldFindLatestContentRankingNewsPerContentBeforeLimit테스트에 동일contentId의 서로 다른visibleFromAtUtcrow, 같은 시각의 서로 다른newsIdrow, 다른 콘텐츠 row를 저장한다.visibleFromAtUtc desc,newsId desc기준 최신 row 하나만 남고 중복 제거 후limit까지 다른 고유 소식이 채워지는지 검증한다. -
RED 확인: 아래 focused test를 실행해 과거 크리에이터 배치 노출,
CONTENT_RANKING미조립 또는 동일 콘텐츠 중복 노출 때문에 assertion이 실패하는지 확인한다../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest" -
GREEN:
DefaultHomeFollowingQueryRepository.findRecentNews(...)에creator_ranking_snapshot의rankingType = WEEKLY,visibleFromAtUtc <= nowUtc중 최신 공개 시각과 일치하는CREATOR_RANKING만 허용하는 조건을 추가한다. 공개 스냅샷이 없으면CREATOR_RANKING을 반환하지 않는다. -
GREEN:
CONTENT_RANKING을 활성 오디오 콘텐츠 target과 조인·조립하고, 동일 회원·동일targetId의 노출 가능한 더 최신 row가 존재하지 않는 항목만 남기는 조건을 추가한다. 최신 비교는visibleFromAtUtc, 동률이면 inboxid를 사용하며 이 조건을 전체limit보다 먼저 적용한다. -
GREEN 확인: 같은 focused test 명령을 다시 실행해 두 회귀 테스트와 기존 repository 테스트가 모두 통과하는지 확인한다.
-
REFACTOR: 이번 Task가 추가한 QueryDSL alias와 조건 함수만 정리하고, 공개 port/DTO·DDL은 변경하지 않는다. 아래 직접 영향 회귀와 lint를 실행해 결과를 기록한다.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest" ./gradlew --no-daemon ktlintCheck
-
Phase 7 Gate
Goal 실행 P7-GATE: Phase 7의 최신 배치·콘텐츠 중복 제거 정책과 기존 팔로잉 탭 API 회귀를 최종 판정한다.
- 시작 조건:
P7-T1완료. - 완료 증거: 아래 명령이 모두
BUILD SUCCESSFUL이고 결과가## 6. 검증 기록에 누적됨. - 범위 밖: 전체 회귀 실패와 무관한 코드 수정, 테스트 삭제·완화,
CONTENT_RANKING발행 기능 추가.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"
./gradlew --no-daemon ktlintCheck
Expected: 과거 크리에이터 랭킹 배치와 동일 콘텐츠의 중복 랭킹이 최근 소식에 포함되지 않고, 기존 팔로잉 탭 응답 계약과 다른 소식 타입 회귀가 없다.
-
전체
./gradlew test는 조회 repository 한 파일과 해당 테스트만 변경하는 국소 보완이므로 기본 Gate에서 생략한다. focused test 또는 E2E에서 공유 경계 회귀를 판단할 수 없는 실패가 발생하면 전체 회귀로 확장하고 근거와 결과를 기록한다. -
Task 7.2: 빈 최신 크리에이터 랭킹 배치에서 과거 소식 제외
Goal 실행
P7-R1:REV-P7-001에 따라 최신 완료 배치의 결과가 0건이어도 이전 배치의 크리에이터 랭킹 소식을 노출하지 않는다.- 시작 조건:
reviews/phase-7-review.md의REV-P7-001확정, 기존P7-GATE완료. - 완료 증거: 빈 최신 배치 재현 테스트의 RED 확인, 최소 구현 후 repository/E2E 회귀 통과, 검증 기록 누적.
- 범위 밖: 랭킹 집계·점수 정책 변경,
CONTENT_RANKING발행, 신규 테이블/공개 API 변경. - Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt
- Modify:
- RED: 직전 공개 스냅샷/inbox와 더 최신
WEEKLY,DONEsnapshot job을 저장하되 최신 배치 스냅샷은 0건으로 두고, 과거CREATOR_RANKING이 제외되어야 하는 테스트를 추가해 실패를 확인한다. - GREEN: 최신 공개 배치 식별은 결과 row가 없어도 남는
creator_ranking_snapshot_job의WEEKLY,DONE,visibleFromAtUtc <= nowUtc최신 시각을 기준으로 한다. 기존 snapshot만 있고 적용 가능한 job 이력이 전혀 없는 데이터의 호환 fallback이 필요하면 그 경우로만 제한하며, 빈DONE배치에서는 과거 snapshot으로 fallback하지 않는다. - GREEN 확인: repository focused test를 재실행해 최신 빈 배치와 기존 신규 공개 전/후 정책이 모두 통과하는지 확인한다.
- REFACTOR: 최신 배치 조건 helper와 필요한 QueryDSL alias만 정리하고 port/DTO/DDL은 변경하지 않는다.
- 시작 조건:
-
Task 7.3: 노출 가능한 콘텐츠 랭킹 row만 최신 중복 제거 기준으로 사용
Goal 실행
P7-R2:REV-P7-002에 따라 더 최신이지만 노출 불가능한CONTENT_RANKINGrow가 이전의 노출 가능한 row를 가리지 않도록 한다.- 시작 조건:
P7-R1완료와reviews/phase-7-review.md의REV-P7-002확정. - 완료 증거:
rank=null또는 회원에게 노출 불가한 최신 row 재현 테스트의 RED 확인, 최소 구현 후 focused test 통과, 검증 기록 누적. - 범위 밖: 콘텐츠 랭킹 발행/스냅샷 연동, 콘텐츠 동일성 기준 변경, 공개 응답 스키마 변경.
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt
- Modify:
- RED: 같은
memberId/targetId에서 이전 row는 노출 가능하고 더 최신 row는rank=null인 fixture를 추가해 이전 row가 유지되어야 하는 테스트를 작성하고 실패를 확인한다. - RED: 비성인 회원에게 더 최신 inbox row만
isAdult=true인 경우에도 이전의 노출 가능한 row가 유지되는 테스트를 작성하고 실패를 확인한다. - GREEN:
latestContentRankingNewsCondition(...)의 newer-row 판정에rank is not null, 회원별 inbox 성인 조건 등 row마다 달라질 수 있는 외부 조회와 동일한 노출 조건을 적용한다. - GREEN 확인: repository focused test를 재실행해 노출 불가 newer row와 기존 최신 시각/id tie-break 회귀가 모두 통과하는지 확인한다.
- REFACTOR: 중복 제거 조건만 최소 수정하고 다른 최근 소식 타입의 조인·조립은 변경하지 않는다.
- 시작 조건:
Phase 7 리뷰 회귀 Gate
Goal 실행 P7-R-GATE: Phase 7 리뷰에서 확정된 빈 배치와 노출 가능 row 기준을 최종 판정한다.
- 시작 조건:
P7-R1,P7-R2완료. - 완료 증거: 아래 명령이 모두
BUILD SUCCESSFUL이고 결과가## 6. 검증 기록과reviews/phase-7-review.md에 누적됨. - 범위 밖: 전체 회귀 실패와 무관한 코드 수정, 테스트 삭제·완화, 신규 발행 기능.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"
./gradlew --no-daemon ktlintCheck
5. 구현 순서 요약
- DTO/domain/controller/security 기본 응답을 먼저 만든다.
- inbox entity/repository/adapter와 unique 정책을 만든다.
- 팔로잉 크리에이터, On Air, 스케줄, 최근 소식 조회 repository를 만든다.
- query service와 facade에서 섹션을 조립한다.
- publish service를 만들고 언팔로우/랭킹/콘텐츠/커뮤니티 이벤트에 연결한다.
FollowingNewsResponse를 타입별 nested DTO 계약으로 전환하고 무료 커뮤니티 게시글만COMMUNITY_POST최근 소식을 발행하도록 보강한다.- End-to-End 테스트와 전체 회귀 검증을 수행한다.
- 후속 회귀 수정으로
CREATOR_RANKING최신 공개 배치 필터와CONTENT_RANKING콘텐츠별 최신 소식 중복 제거를 적용한다. - Phase 3 리뷰 회귀 수정으로 On Air와 라이브 스케줄에 기존 라이브 입장 제한을 적용한다.
- Phase 7 리뷰 회귀 수정으로 빈 최신 랭킹 배치와 노출 불가 콘텐츠 랭킹 newer row를 처리한다.
- Phase 4 리뷰 회귀 수정으로 publish와 팔로우 상태 변경의 동시성 경계를 보장한다.
- Phase 6 리뷰 후속 문서 수정으로
newsId식별자 계약을 현재 구현과 동기화한다.
6. 검증 기록
-
2026-06-25 Phase 1-2 구현 검증:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingControllerTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest"실행 결과BUILD SUCCESSFUL.- 병렬 Gradle 실행 중
build/snapshot/kotlin/kaptGenerateStubsKotlin삭제 충돌이 1회 발생해 동일 명령을 순차 재실행했다.
-
2026-06-25 Phase 3 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과 repository/service 미구현 컴파일 오류로BUILD FAILED. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryServiceTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest"실행 결과BUILD SUCCESSFUL.
- RED 확인:
-
2026-06-25 Phase 4 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNewsSourceKeyTest"실행 결과HomeFollowingNewsSourceKey,HomeFollowingNewsPublishService미구현 및 생성자 의존성 미연동 컴파일 오류로BUILD FAILED. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNewsSourceKeyTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.content.AudioContentServiceTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest"실행 결과BUILD SUCCESSFUL.insertIgnoreAll은 H2/MySQL dialect 분기 없이 JPAsaveAndFlush와 unique 제약 기반 중복 예외 재확인 단일 경로로 검증했다.
- RED 확인:
-
2026-06-25 Phase 5 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacadeTest"실행 결과 facade 생성자 미구현으로BUILD FAILED. - RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과 facade 생성자 미구현으로BUILD FAILED. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacadeTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과BUILD SUCCESSFUL.
- RED 확인:
-
2026-06-26 Phase 3-5 리뷰 보완 검증:
- 리뷰 지적 사항에 따라 팔로잉 탭 조회의 크리에이터 role 필터, 오디오 공개 시각 판정, 커뮤니티 최근 소식 미리보기 마스킹, 최근 소식 발행
REQUIRES_NEW트랜잭션, inboxtitle/body길이 정규화를 보강했다. 이후 계약 변경으로COMMUNITY_POST최근 소식은 무료 게시글만 발행하도록 재정의했다. - RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest" --tests "kr.co.vividnext.sodalive.content.AudioContentServiceTest" --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest"실행 결과 reviewer 보완 전 7개 regression 테스트 실패를 확인했다. - 같은 regression 테스트 명령 재실행 결과
BUILD SUCCESSFUL. - Phase 3-5 전체 대상 테스트 명령 재실행 결과
BUILD SUCCESSFUL. ./gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL.
- 리뷰 지적 사항에 따라 팔로잉 탭 조회의 크리에이터 role 필터, 오디오 공개 시각 판정, 커뮤니티 최근 소식 미리보기 마스킹, 최근 소식 발행
-
2026-06-26 Phase 3-5 2차 리뷰 보완 검증:
- 2차 리뷰 지적 사항에 따라 inbox insert 정상 경로를 row별
saveAndFlush에서 기존 memberId 일괄 조회 +saveAll+ 단일flush로 완화하고, 중복 충돌 fallback은 유지했다. - 유료 오디오 콘텐츠의
isFullDetailVisible=false상세 설명은 기존 상세 API 정책과 동일하게 미리보기만 최근 소식에 저장하도록 보강했다. - 오디오/커뮤니티/랭킹 최근 소식 발행 실패가 원 업로드/게시글 생성/랭킹 스냅샷 갱신 성공을 실패로 전파하지 않도록 after-commit 발행 예외를 로그로 격리했다.
- 보완 직후 regression 테스트에서 adapter race 테스트와 Mockito matcher stubbing 불일치 실패를 확인한 뒤 테스트를 새 구현 경로에 맞게 정리했다.
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.content.AudioContentServiceTest" --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest" --tests "kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest"실행 결과BUILD SUCCESSFUL.- Phase 3-5 전체 대상 테스트 명령 재실행 결과
BUILD SUCCESSFUL. ./gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL.
- 2차 리뷰 지적 사항에 따라 inbox insert 정상 경로를 row별
-
2026-06-26 Phase 3-5 3차 리뷰 보완 검증:
- 최근 소식 조회가
AUDIO_CONTENT,COMMUNITY_POST원천 target의isActive=false상태를 최종 제외하도록 보강했다.CREATOR_RANKING은 creator 활성/role 필터를 유지하고, 아직 원천 테이블이 없는 예약 타입은 조회에서 노출하지 않는다. - 이달의 스케줄 정렬을
scheduledAtUtc asc,type.sortOrder asc,targetId asc로 안정화했다. - inbox insert를 H2/MySQL 공통 JPA portable path로 변경했다. 구현은
newsType/sourceKey별 기존 수신 member id를 일괄 조회한 뒤 신규 row만saveAll+flush하고, unique 충돌 시 persistence context를 정리한 뒤 한 번 재조회/재시도한다. - 추후 운영에서 follower 수가 큰 크리에이터 이벤트로
member_id in (...)또는saveAll배치 크기가 병목이 되면, follower id chunking, outbox table, 비동기 worker, 재시도/모니터링 대시보드 도입을 별도 후속 작업으로 진행한다. - RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest" --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterRetryTest"실행 결과 target 비활성 필터와 insert retry 미구현으로BUILD FAILED. - 같은 regression 테스트 명령 재실행 결과
BUILD SUCCESSFUL. - Phase 3-5 전체 대상 테스트 명령 재실행 결과
BUILD SUCCESSFUL. ./gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test전체 테스트 실행 결과BUILD SUCCESSFUL.
- 최근 소식 조회가
-
2026-06-26 Phase 6 문서/회귀 검증:
- 문서 동기화 확인을 위해
rg -n "FollowingNewsRankingResponse|ranking\\?|rankChange|isNew|creatorId" docs/20260625_메인_홈_팔로잉_탭_API를 실행했다. 검색 결과의creatorId는 팔로잉 크리에이터/스케줄 공개 필드, 최근 소식의creatorId부재 검증 설명, 내부creator_id/port 인자/테스트 설명 맥락으로 확인했으며 삭제된 공개 응답 필드 잔존은 확인되지 않았다. ./gradlew tasks --all실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test실행 결과BUILD SUCCESSFUL../gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL.
- 문서 동기화 확인을 위해
-
2026-06-30 Phase 5.5 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest"실행 결과 nested domain/DTO 미구현 컴파일 오류로BUILD FAILED. - RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest.shouldPopulateRecentNewsNestedPayloadsFromSourceTargets" --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest.shouldExcludePaidCommunityPostRecentNews"실행 결과 원천 payload enrichment와 유료 커뮤니티 제외 미구현으로BUILD FAILED. - RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest.shouldNotPublishNewsAfterPaidCommunityPostCreated"실행 결과 유료 커뮤니티 게시글도 최근 소식을 발행해BUILD FAILED. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest.shouldPopulateRecentNewsNestedPayloadsFromSourceTargets" --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest.shouldExcludePaidCommunityPostRecentNews"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest.shouldPublishNewsAfterCommunityPostCreated" --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest.shouldNotPublishNewsAfterPaidCommunityPostCreated"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest.shouldAssembleFollowingTabForMember"실행 결과BUILD SUCCESSFUL.- Phase 5.5 집중 회귀 명령
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest" --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest" --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과BUILD SUCCESSFUL. ./gradlew --no-daemon ktlintCheck실행 결과 import 정렬 위반 2건을 수정한 뒤 재실행 결과BUILD SUCCESSFUL.- 리뷰 보완:
CREATOR_RANKING은rank가 있는 row만 노출하도록 수정했고,AUDIO_CONTENT/COMMUNITY_POST최근 소식은 현재 원천 target의 성인 상태를 함께 반영하도록 보강했다. - 리뷰 보완:
HomeFollowingEndToEndTest가CREATOR_RANKING,AUDIO_CONTENT, 무료COMMUNITY_POST, 유료 커뮤니티 미노출 JSON surface를 모두 검증하도록 fixture/assertion을 확장했다. - 리뷰 보완 후
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과BUILD SUCCESSFUL. - 리뷰 보완 후
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과BUILD SUCCESSFUL. - 리뷰 보완 후 Phase 5.5 집중 회귀 명령
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponseTest" --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest" --tests "kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과BUILD SUCCESSFUL. - 리뷰 보완 후
./gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL. - 응답 단순화:
HomeFollowingContentNews/FollowingContentNewsResponse의releaseDate를 제거하고 콘텐츠 공개 시각은 최상위visibleFromAtUtc를 사용하도록 계약과 테스트를 갱신했다.
- RED 확인:
-
2026-07-12 팔로잉 크리에이터 정렬 변경 영향 확인:
DefaultHomeFollowingQueryRepository.findFollowingCreators의 정렬을creatorFollowing.createdAt.desc()에서creatorFollowing.createdAt.asc()로 변경한 계약에 맞춰 PRD/계획 문서의followingCreators정렬 설명을 오래된 팔로우순으로 갱신했다.- 직접 검색 결과 정렬 계약을 명시한 위치는
docs/20260625_메인_홈_팔로잉_탭_API/prd.md,docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md,DefaultHomeFollowingQueryRepositoryTest.shouldFindActiveFollowingCreatorsByOldestFollowOrder였고, 호출부는HomeFollowingQueryService가 반환 순서를 그대로 조립하는 구조로 확인했다. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과BUILD SUCCESSFUL../gradlew --no-daemon tasks --all실행 결과BUILD SUCCESSFUL.
-
2026-07-30 Phase 7 요구사항·계획 문서 반영:
- PRD에
CREATOR_RANKING최신 공개WEEKLY배치 한정, 신규 배치 공개 전 직전 배치 유지, 최신 배치에 없는 크리에이터의 과거 순위 미보충 정책을 추가했다. - PRD에
CONTENT_RANKING의 동일contentId별 최신 항목 한 건 조회와 발행 기능 제외 정책을 추가했다. plan-task.md에 미완료 회귀 수정 GoalP7-T1과 Phase GateP7-GATE를 추가하고 RED → GREEN → REFACTOR, focused test, 직접 영향 E2E, lint 검증 명령을 연결했다.rg -n -S "최신 공개|과거.*랭킹|동일.*contentId|CONTENT_RANKING.*발행|P7-T1|P7-GATE|Task 7\\.1|Phase 7" docs/20260625_메인_홈_팔로잉_탭_API/prd.md docs/20260625_메인_홈_팔로잉_탭_API/plan-task.md로 두 정책과 Goal 연결을 확인했다.git diff --check실행 결과 오류가 없었다../gradlew tasks --all최초 실행은 sandbox의 사용자 Gradle 캐시 접근 제한으로 실패했으며, 동일 명령을 승인된 권한으로 재실행해BUILD SUCCESSFUL을 확인했다.- 이번 요청은 문서 반영만 수행했으므로
P7-T1구현 체크박스와 Phase 7 상태는 대기로 유지했다.
- PRD에
-
2026-07-30 Phase 7 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과shouldFindOnlyLatestVisibleCreatorRankingBatchInRecentNews,shouldFindLatestContentRankingNewsPerContentBeforeLimitassertion 실패로BUILD FAILED. DefaultHomeFollowingQueryRepository.findRecentNews(...)에 최신 공개WEEKLY크리에이터 랭킹 배치 필터,CONTENT_RANKING활성 오디오 target 조립, 동일 contentId 최신 row 조건을 추가했다. 공개 port/DTO/DDL은 변경하지 않았다.- GREEN 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과BUILD SUCCESSFUL. - 직접 영향 E2E 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"최초 실행은 E2E fixture에 최신 공개creator_ranking_snapshot이 없어BUILD FAILED; fixture 보강 후 재실행 결과BUILD SUCCESSFUL. - Gate lint 확인:
./gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL.
- RED 확인:
-
2026-07-30 Phase 1~7 정적 코드 리뷰:
- 사용자 요청에 따라 컴파일과 테스트는 재실행하지 않고 PRD, 구현 계획, production/test 코드, 기존 검증 기록을 정적으로 대조했다.
- 문서 명령 유효성 확인을 위한
./gradlew tasks --all최초 실행은 sandbox의 사용자 Gradle cache 접근 제한으로 실패했고, 승인된 권한으로 같은 명령을 재실행해BUILD SUCCESSFUL을 확인했다. 이 명령은 컴파일과 테스트를 실행하지 않는다. - Phase별 판정은
docs/20260625_메인_홈_팔로잉_탭_API/reviews/phase-1-review.md부터phase-7-review.md까지 기록했으며, Phase 5.5는phase-5.5-review.md로 분리했다. - Phase 3의 라이브 입장 제한 누락
REV-P3-001을 확정하고P3-R1,P3-R-GATE를 추가했다. - Phase 7의 빈 최신 크리에이터 랭킹 배치 판정
REV-P7-001과 노출 불가 콘텐츠 랭킹 newer row 판정REV-P7-002를 확정하고P7-R1,P7-R2,P7-R-GATE를 추가했다. - Phase 1, 2, 4, 5, 5.5, 6은 이번 정적 리뷰 범위에서 확정 발견 사항이 없다.
-
2026-07-30 Phase 3·7 리뷰 보완 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.DefaultHomeFollowingQueryRepositoryTest"실행 결과findOnAirLives/findMonthlySchedules의 effective gender·creator 여부 인자 미반영 컴파일 오류로BUILD FAILED. HomeFollowingQueryService에서 본인인증 성별 우선 effective gender와 크리에이터 여부를 port로 전달하고,DefaultHomeFollowingQueryRepository의 On Air·라이브 스케줄에genderRestriction/isAvailableJoinCreator조건을 적용했다.CREATOR_RANKING최신 배치 식별을creator_ranking_snapshot_job의 최신WEEKLY,DONE공개 시각 기준으로 보강해 최신 빈 배치에서 과거 랭킹을 보충하지 않도록 했다.CONTENT_RANKING중복 제거의 newer row 판정에rank is not null과 회원별 inbox 성인 조건을 추가해 노출 불가 row가 이전 노출 가능 row를 가리지 않도록 했다.- GREEN 확인: 위 focused test 명령 재실행 결과
BUILD SUCCESSFUL. - E2E 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과BUILD SUCCESSFUL. - Gate lint 확인:
./gradlew --no-daemon ktlintCheck최초 실행은 import 정렬 위반으로BUILD FAILED; import 정렬 수정 후 재실행 결과BUILD SUCCESSFUL. - 리뷰 게이트: Oracle reviewer가 P3-R1, P7-R1, P7-R2, 공개 API/DDL 비확장, 테스트/문서 기록을 검토했고 Critical/Important blocker 없음으로 승인했다.
- RED 확인:
-
2026-07-30 Phase 1~7 2차 정적 코드 리뷰:
- 사용자 요청에 따라 컴파일과 테스트를 실행하지 않고 PRD, 구현 계획, production/test 코드, 기존 검증 기록을 다시 대조했다.
- Phase 3의
P3-R1과 Phase 7의P7-R1·P7-R2반영 코드 및 기존 Gate 성공 기록을 정적으로 재확인했다. - Phase 4에서 active follower 조회와 inbox insert 사이에 언팔로우가 완료되면 뒤늦게 active inbox가 생성되고, 이후 재팔로우 시 언팔로우 이전 이벤트가 노출될 수 있는
REV-P4-001을 확정했다. - Phase 6에서 PRD는
newsId를{TYPE}:{targetId}로 설명하지만 repository와 테스트는 inbox PK의 10진 문자열을 사용하는REV-P6-001을 확정했다. - 후속 작업으로 Task 4.6 /
P4-R1/P4-R-GATE와 Task 6.3 /P6-R1/P6-R-GATE를 추가했다. - Phase 1, 2, 3, 5, 5.5, 7에서는 이번 2차 정적 리뷰의 신규 확정 발견 사항이 없다.
- 이번 리뷰에서는 Gradle 명령을 실행하지 않았으며, 문서 변경은 정적 검색과
git diff --check로만 점검한다.
-
2026-07-30 Phase 4·6 리뷰 보완 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest.shouldNotExposeNewsCreatedBeforeUnfollowAfterRefollowWhenPublishIsDelayed"실행 결과 stale active inbox assertion 실패로BUILD FAILED. creator_followingactive follower 조회에for update를 적용하고,MemberService.creatorFollow(...)/creatorUnFollow(...)가 같은 row를PESSIMISTIC_WRITE로 조회하도록 보강했다. 공개 API·port·DDL은 변경하지 않았다.- GREEN 확인: 위 동시성 단일 테스트 재실행 결과
BUILD SUCCESSFUL. - P4-R1 focused test
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest" --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"실행 결과BUILD SUCCESSFUL. - P4-R-GATE E2E
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest"실행 결과BUILD SUCCESSFUL. - P4-R-GATE lint
./gradlew --no-daemon ktlintCheck최초 실행은 테스트 import 정렬 위반으로BUILD FAILED; import 정렬 수정 후 재실행한다. - P4-R-GATE lint 재실행 결과
BUILD SUCCESSFUL. - P6-R1 문서 보완으로 PRD에서
scheduleId는{TYPE}:{targetId},newsId는home_following_news_inbox.id의 10진 문자열이며 정렬·동률 해소에 사용하는 계약으로 분리했다. - P6-R-GATE 정적 검색
rg -n "newsId|scheduleId|home_following_news_inbox\.id" ...실행 결과 PRD, repository, repository test, E2E가scheduleId는{TYPE}:{targetId},newsId는 inbox PK 문자열 계약으로 일치함을 확인했다. - P6-R-GATE
git diff --check실행 결과 오류 없음. - P6-R-GATE
./gradlew tasks --all실행 결과BUILD SUCCESSFUL. - 리뷰 게이트: Oracle reviewer가 P4 동시성 보완과 P6 문서 계약 동기화를 검토했고 Critical/Important/Minor blocker 없음으로 승인했다.
- RED 확인:
-
2026-07-30 Phase 1~7 3차 정적 코드 리뷰:
- 사용자 요청에 따라 컴파일과 테스트를 실행하지 않고 PRD, 구현 계획, production/test 코드, 기존 리뷰·검증 기록을 Phase별로 다시 대조했다.
- Phase 4에서 공개
POST /member/creator/follow가isActive=false를 전달해도 관계가 없으면 새 active 팔로우를 만들고, 기존 관계에서는 inbox 비활성화를 호출하지 않아 재팔로우 시 과거 소식이 다시 노출될 수 있는REV-P4-002를 확정했다. - Phase 6에서 PRD의 최신
CREATOR_RANKING배치 설명이 snapshot만 기준으로 적혀 있어,P7-R1로 반영된 최신WEEKLY,DONEjob 우선·legacy snapshot 제한 fallback 동작과 불일치하는REV-P6-002를 확정했다. - 후속 작업으로 Task 4.7 /
P4-R2/P4-R2-GATE와 Task 6.4 /P6-R2/P6-R2-GATE를 추가했다. - Phase 1, 2, 3, 5, 5.5, 7에서는 이번 3차 정적 리뷰의 신규 확정 발견 사항이 없다.
- 이번 리뷰에서는 Gradle 명령을 실행하지 않았으며, 문서 변경은 정적 검색과
git diff --check로만 점검한다.
-
2026-07-30 Phase 4 3차 리뷰 보완 구현 검증:
- RED 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.member.MemberServiceTest"실행 결과creatorFollow(..., isActive=false)신규 회귀 2건이 assertion 실패로BUILD FAILED. - 관계가 없고
isActive=false인 통합 팔로우 요청은 새 active 관계를 만들지 않고, 기존 관계를 inactive로 바꾸는 경로에서는homeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(...)를 호출하도록MemberService.creatorFollow(...)만 최소 수정했다. - GREEN 확인: 같은
MemberServiceTest명령 재실행 결과BUILD SUCCESSFUL. - P4-R2-GATE 확인:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInboxPersistenceAdapterTest" --tests "kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishServiceTest"실행 결과BUILD SUCCESSFUL. - P4-R2-GATE lint
./gradlew --no-daemon ktlintCheck최초 실행은 신규 테스트 줄 길이 위반으로BUILD FAILED; 포맷 정리 후MemberServiceTest와ktlintCheck재실행 결과 모두BUILD SUCCESSFUL.
- RED 확인:
-
2026-07-30 Phase 6 3차 리뷰 보완 문서 검증:
- PRD Feature F와 최근 소식 Inbox 기술 제약의
CREATOR_RANKING최신 공개 배치 설명을 현재 구현의creator_ranking_snapshot_job최신WEEKLY,DONE,visibleFromAtUtc <= nowUtc우선 기준과 일치하도록 갱신했다. - 적용 가능한 완료 job이 전혀 없는 legacy/backfill 데이터에서만
creator_ranking_snapshotfallback을 허용하고, 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다고 명시했다. - P6-R2-GATE 정적 검색
rg -n "creator_ranking_snapshot_job|creator_ranking_snapshot|최신 공개 배치|WEEKLY|DONE|legacy|fallback" ...실행 결과 PRD, Task 7.2, repository, 빈 최신 배치 회귀 테스트의 기준을 대조했다. 검색 결과의 과거 Task 7.1 snapshot 설명은 Task 7.2에서 superseded된 완료 기록으로 확인했다. - P6-R2-GATE
git diff --check실행 결과 오류 없음. - P6-R2-GATE
./gradlew tasks --all실행 결과BUILD SUCCESSFUL. - 추가 전체 회귀 확인:
./gradlew --no-daemon test실행 결과BUILD SUCCESSFUL. - 리뷰 게이트: Oracle reviewer가 P4-R2 통합 언팔로우 보완과 P6-R2 문서 동기화를 검토했고 Critical/Important/Minor blocker 없음으로 승인했다.
- PRD Feature F와 최근 소식 Inbox 기술 제약의
-
2026-07-30 Phase 1~7 4차 정적 코드 리뷰:
- 사용자 요청에 따라 컴파일과 테스트를 실행하지 않고 PRD, 구현 계획, DDL, production/test 코드와 기존 리뷰 후속 구현을 Phase별로 다시 대조했다.
- Phase 1은 공개 endpoint·비회원 응답·DTO, Phase 2는 inbox 저장·중복·잠금, Phase 3은 섹션별 조회·라이브 제한을 확인했다.
- Phase 4는 after-commit 발행·팔로우 상태 동시성·통합 inactive 경로, Phase 5와 5.5는 facade/E2E·nested payload·무료 커뮤니티 정책을 확인했다.
- Phase 6은 문서·식별자·latest batch 계약, Phase 7은 최신 빈 배치와 노출 가능한 콘텐츠 랭킹 중복 제거를 확인했다.
- 각 Phase의 4차 판정은
reviews/phase-1-review.md부터phase-7-review.md까지 누적했으며 Phase 5.5는 별도 보고서에 기록했다. - 이번 차수의 신규 확정 발견 사항이 없어 신규 회귀 수정 Task/Goal은 추가하지 않았고 기존 Phase 완료 판정을 유지한다.
- 이번 리뷰에서는 Gradle 명령을 실행하지 않았으며, 문서 변경은 정적 검색과
git diff --check로만 점검한다.