# 크리에이터 채널 커뮤니티 아이템 통일 구현 계획 > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. 프로젝트 보수적 실행 모드에 따라 multi-agent는 사용하지 않는다. **Goal:** 홈과 커뮤니티 탭의 커뮤니티 아이템 외형·상세 진입·더보기를 통일하고 구매는 상세에서만 처리하며, 유료 미구매 상태의 reaction·댓글·더보기 노출을 차단한다. **Architecture:** 홈 API 게시글을 private 로컬 변환으로 `CreatorChannelCommunityPostItem`에 연결해 기존 `CreatorChannelCommunityListItem`을 재사용한다. 목록 구매 상태·dialog는 상세 ViewModel·View로 옮기고 기존 구매 API를 재사용하며, 변경 성공 후 홈·커뮤니티 목록을 동기화한다. 목록·홈 공용 아이템과 상세는 유료 잠금 상태에서 reaction bar를 제외하고, 공용 리스트 아이템은 더보기도 제외한다. 상세 View·ViewModel은 하나의 댓글 상호작용 상태로 댓글 UI와 mutation을 차단한다. **Tech Stack:** Swift, SwiftUI, Combine, Moya, Kingfisher, SDWebImageSwiftUI, Xcode workspace `SodaLive.xcworkspace`. ## Global Constraints - 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**`에서 해결하고 신규 dependency를 추가하지 않는다. - 기존 `CommunityPostCard`와 메인 홈 팔로잉·추천 사용처는 수정하지 않는다. - 백엔드는 홈 API의 `notices`·`communities` 아이템에 non-optional `isCommentAvailable: Bool`·`isPinned: Bool`을 제공한다. - 홈 API의 `dateUtc`는 유지하고 iOS에서 값 변환 없이 `createdAtUtc`로 연결한다. - 목록의 리스트형·썸네일형 구매 호출은 제거하고 기존 구매 dialog·API 호출을 상세로 옮긴다. - 유료 미구매 영역은 홈·리스트형·썸네일형·상세 모두 `Color.gray800`을 사용한다. - 하트·댓글을 모두 숨기는 유료 미구매 상태에서는 reaction bar의 고정 높이와 부모 spacing을 남기지 않는다. - 상세의 유료 미구매 상태에서는 댓글 구분선·목록·입력창을 표시하지 않고 작성·수정·삭제 API 요청을 시작하지 않는다. - 현재 실행 가능한 단일 test bundle이 없으므로 신규 test 타겟을 만들지 않고 정적 contract check·Preview·Debug build·시뮬레이터로 검증한다. - 사용자가 직접 요청하지 않았으므로 `git commit`은 실행하지 않는다. --- | 문서 항목 | 내용 | |---|---| | 상태 | `P2-R3` 상태별 수동 검증 완료·`P2-R4` 코드/자동 검증 완료·상태별 수동 확인 대기 | | 작성일 | 2026-08-10 | | 요구사항 기준 | `docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md` | | API 기준 | `GET /api/v2/creator-channels/{creatorId}/home` 필드 확장·기존 커뮤니티 목록/상세 API | | 현재 Phase | Phase 2 | | 현재 활성 Goal | `P2-R4`—더보기 상태별 수동 확인 | ## 목표 홈 요약·커뮤니티 리스트·썸네일에서 게시글 터치 결과와 잠금 스타일을 통일하고, 더보기 권한 메뉴와 상세 구매 흐름을 정확하게 유지한다. 상세의 유료 미구매 방문자에게는 reaction·댓글 영역을 표시하지 않고 댓글 mutation을 허용하지 않는다. ## 현재 상태 | Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| | 1 | 완료 | `3/3` | `P1-GATE` 완료 | 없음 | | 2 | 진행 | `5/6` | `P2-R4` | 상태별 수동 확인 | | 3 | 진행 | `0/1` | `P3-T1`, `P3-GATE` | `EXT-001` 반영 | ## 범위 ### 포함 - 홈 커뮤니티 응답 필드·Preview fixture 반영 - 리스트형·썸네일형 목록 구매 제거와 상세 진입 통일 - 홈 섹션의 `CreatorChannelCommunityListItem` 재사용 - 홈의 고정·수정·삭제·신고 메뉴 연결과 성공 후 목록 동기화 - 홈·커뮤니티 리스트와 상세에서 표시할 reaction이 없을 때 빈 공간 제거 - 커뮤니티 상세의 유료 미구매 상태에서 댓글 구분선·목록·입력창 미표시와 작성·수정·삭제 mutation 차단 - 정적 검사·Preview·Debug build·시뮬레이터 검증·문서 누적 ### 제외 - `CommunityPostCard`와 메인 홈 팔로잉·추천 섹션 변경 - 구매 endpoint와 `CommunityPostPurchaseDialog` 내부 UI 변경 - 커뮤니티 작성·상세·댓글의 범위 밖 리팩터링 - 신규 공용 모델·protocol·factory·dependency·test target 추가 - 백엔드 구현과 배포 - 상세 API 댓글 payload 계약과 `CreatorChannelReplyDetail/**` 변경 ## 기술적 제약 - 홈 응답·탭 응답 모델은 통합하지 않고 `CreatorChannelCommunitySection` private 변환으로 연결한다. - 목록 구매 제거로 새로 미사용된 상태·메서드·dialog 분기만 제거한다. - 홈·커뮤니티 변경 성공 후 갱신은 `CreatorChannelView`의 한 helper에서 두 ViewModel 재조회를 호출한다. - 사용자 노출 오류는 기존 `errorMessage`·`isShowPopup`·toast 흐름을 유지한다. - 홈 응답에 추가 필드가 없으면 decoding 실패를 기존 홈 API 실패로 처리하고 fallback 값을 만들지 않는다. - 댓글 차단은 기존 유료 잠금 조건 `price > 0 && existOrdered == false && isOwnPost == false`와 상세의 `isCommentAvailable`을 함께 사용한다. ## Task 검증 규칙 - 현재 프로젝트에 실행 가능한 unit test bundle이 없어 SwiftUI View·Decodable 변경에 대한 신규 unit test를 추가하지 않는다. - 각 구현 Task는 변경 전 구조를 재현하는 실행 가능한 `rg` contract check를 **RED**로 실행한다. - 최소 구현 후 같은 contract check의 기대 결과가 반전되는지 확인하고, 영향 범위 `xcodebuild`로 **GREEN**을 판정한다. - Preview·시뮬레이터 검증은 시각·gesture 동작이 unit test target 없이 자동화되지 않는 부분의 대체 완료 증거로 사용한다. ## Phase 1: 홈 계약과 목록 상호작용 **Phase 결과:** 홈 응답이 필요한 고정·댓글 상태를 디코딩하고, 커뮤니티 리스트형·썸네일형은 상세로 이동하며 구매 dialog·API는 상세에서 동작한다. **선행조건:** PRD `CCCI-004`, `CCCI-005`, `CCCI-007`, `CCCI-008`과 `EXT-001` 계약 확정. **Phase 완료 조건:** `P1-T1`, `P1-T2`, `P1-T3`, `P1-GATE` 완료. ### Task 1.1 홈 커뮤니티 응답 계약 반영 **Goal 실행 `P1-T1`:** 홈 커뮤니티·공지 아이템이 `isCommentAvailable`·`isPinned`을 non-optional Bool로 디코딩한다. - **시작 조건:** `EXT-001` 필드명·타입 확정. - **완료 증거:** 모델·Preview fixture 반영, RED/GREEN contract check, `SodaLive-dev` Debug build. - **범위 밖:** 백엔드 구현, 모델 통합, fallback 기본값. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift` - Test: 신규 test 파일 없음—기존 Preview fixture·contract check·Debug build 사용 **Interfaces:** - Consumes: 홈 API `communities`·`notices` 아이템의 `isCommentAvailable: Bool`·`isPinned: Bool`. - Produces: `CreatorChannelCommunityPostResponse.isCommentAvailable: Bool`, `CreatorChannelCommunityPostResponse.isPinned: Bool`. - [x] **RED:** 홈 응답 모델에 두 필드가 없음을 확인한다. ```bash rg -n "let (isCommentAvailable|isPinned): Bool" SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift ``` Expected: match 0건, exit code 1. - [x] **RED 확인:** 현재 Preview fixture가 두 필드 인자를 전달하지 않음을 확인한다. ```bash rg -n "isCommentAvailable:|isPinned:" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift ``` Expected: match 0건, exit code 1. - [x] **GREEN:** `CreatorChannelCommunityPostResponse` 마지막에 두 필드를 추가하고 세 개 Preview fixture에 명시적 샘플 값을 전달한다. ```swift let commentCount: Int let isLiked: Bool let isCommentAvailable: Bool let isPinned: Bool ``` Preview 규칙: ```swift isCommentAvailable: index != 3, isPinned: index == 1 ``` `CreatorChannelNoticeSection` fixture는 각 아이템에 `isCommentAvailable: true`·`isPinned: true/false`를 명시한다. - [x] **GREEN 확인:** 두 필드·세 fixture가 있고 Debug build가 성공함을 확인한다. ```bash rg -n "let (isCommentAvailable|isPinned): Bool" SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift rg -n "isCommentAvailable:|isPinned:" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 모델 2건·fixture 6건 이상 match, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** 추가 변환 타입·optional fallback·주석을 만들지 않고 `git diff --check` 결과를 누적한다. ### Task 1.2 목록 구매 제거와 잠금 배경 통일 **Goal 실행 `P1-T2`:** 커뮤니티 리스트형·썸네일형의 유료 미구매 영역이 `Color.gray800`으로 보이고 터치 시 상세로 이동한다. - **시작 조건:** `P1-T1` 완료. - **완료 증거:** 목록 구매 심볼 0건, `Color.gray400` 잠금 배경 0건, 썸네일 유료 Preview, Debug build. - **범위 밖:** 상세 구매 연결은 `P1-T3`에서 처리하고, 구매 endpoint·dialog 내부 UI는 변경하지 않는다. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` - Test: `CreatorChannelCommunityGridItem_Previews` in the grid item file·contract check·Debug build **Interfaces:** - Consumes: 기존 `onTapDetail`, `CreatorChannelCommunityPostDetailView` 구매 흐름. - Produces: `CreatorChannelCommunityListItem` init without `onTapPurchase`, `CreatorChannelCommunityGridItem` init without `onTapPurchase`. - [x] **RED:** 목록 구매 상태·callback·dialog가 남아 있음을 확인한다. ```bash rg -n "onTapPurchase|openPurchaseDialog|isShowPostPurchaseView|selectedPostPrice|purchaseCommunityPost" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift ``` Expected: 기존 호출처·상태·메서드 match, exit code 0. - [x] **RED 확인:** 리스트형·썸네일형 잠금 배경이 `Color.gray400`인 것을 확인한다. ```bash rg -n "\.fill\(Color\.gray400\)|^[[:space:]]*Color\.gray400[[:space:]]*$" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift ``` Expected: 잠금 배경 2건 match, exit code 0. - [x] **GREEN:** 두 아이템의 `onTapPurchase`를 제거하고 잠금 영역을 상세 진입 gesture에 포함한다. ```swift private var paidLockedImage: some View { RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous) .fill(Color.gray800) .frame(maxWidth: .infinity) .frame(height: 236) .overlay(paidPriceOverlay) } ``` Grid `ZStack`은 잠금/접근 가능 분기 밖에 하나의 gesture를 둔다. ```swift .contentShape(Rectangle()) .onTapGesture(perform: onTapDetail) ``` 썸네일 유료 잠금 상태를 고정된 fixture로 확인할 Preview를 추가한다. ```swift struct CreatorChannelCommunityGridItem_Previews: PreviewProvider { static var previews: some View { CreatorChannelCommunityGridItem( post: CreatorChannelCommunityPostItem( postId: 1, creatorId: 10, creatorNickname: "크리에이터 이름", creatorProfileUrl: "https://picsum.photos/600", createdAtUtc: "2026-08-10T00:00:00Z", content: "유료 커뮤니티 게시글", imageUrl: nil, audioUrl: nil, price: 30, isCommentAvailable: true, likeCount: 5, commentCount: 2, isPinned: true, existOrdered: false, isLiked: false ), isOwnPost: false, onTapDetail: {} ) .frame(width: 134, height: 134) .background(Color.black) .previewLayout(.sizeThatFits) } } ``` `CreatorChannelCommunityViewModel`에서 `isShowPostPurchaseView`, `selectedPostPrice`, `openPurchaseDialog`, `purchaseCommunityPost`를 제거하고 `CreatorChannelView.communityOverlay`의 목록 구매 dialog 분기를 제거한다. - [x] **GREEN 확인:** 목록 구매 심볼·`Color.gray400`이 사라지고 두 아이템에 `Color.gray800`이 있음을 확인한다. ```bash rg -n "onTapPurchase|openPurchaseDialog|isShowPostPurchaseView|selectedPostPrice|purchaseCommunityPost" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift rg -n "\.fill\(Color\.gray400\)|^[[:space:]]*Color\.gray400[[:space:]]*$" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift rg -n "Color\.gray800" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 첫 `rg`와 잠금 배경 `rg` 각각 match 0건·exit code 1, 세 번째 `rg` 두 파일 match, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** 목록 구매 제거로 새로 미사용된 import·state만 정리하고 상세 구매 연결은 `P1-T3`에 남긴다. ### Task 1.3 기존 구매 흐름을 상세로 이동 **Goal 실행 `P1-T3`:** 상세의 유료 미구매 잠금 영역에서 기존 구매 다이얼로그·API가 동작하고 성공 후 상세·홈·커뮤니티 목록을 갱신한다. - **시작 조건:** `P1-T2` 완료. - **완료 증거:** `showPurchaseError` 0건, 상세 구매 dialog·API 연결, Debug build. - **범위 밖:** 구매 endpoint, `CommunityPostPurchaseDialog` 내부 UI, 결제 정책 변경. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift` - Test: contract check·Debug build·시뮬레이터 상세 구매 확인 **Interfaces:** - Consumes: `CreatorCommunityRepository.purchaseCommunityPost(postId:)`, `CommunityPostPurchaseDialog`, `onCommunityRefresh`. - Produces: `isShowPurchaseDialog`, `presentPurchaseDialog()`, `purchaseCommunityPost()`. - [x] **RED:** 상세 잠금 영역이 구매 대신 공통 오류만 표시하고 상세 구매 상태·메서드가 없음을 확인한다. ```bash rg -n "showPurchaseError|onTapPurchase: showPurchaseError" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift rg -n "isShowPurchaseDialog|CommunityPostPurchaseDialog|func (presentPurchaseDialog|purchaseCommunityPost)" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift ``` Expected: 첫 명령 match, 둘째 명령 match 0건·exit code 1. - [x] **GREEN:** 상세 View에 기존 dialog를 표시하고 상세 ViewModel에 기존 구매 API 호출을 연결한다. ```swift @Published var isShowPurchaseDialog = false func presentPurchaseDialog() { guard let detail, detail.price > 0, detail.existOrdered == false, isOwnPost == false else { return } isShowPurchaseDialog = true } func purchaseCommunityPost() ``` 구매 성공 분기는 `fetchDetail(postId:)`과 `onCommunityRefresh?()`를 호출한다. 상세 View는 `onTapPurchase: viewModel.presentPurchaseDialog`와 아래 기존 dialog를 연결한다. ```swift CommunityPostPurchaseDialog( isShowing: $viewModel.isShowPurchaseDialog, can: detail.price, confirmAction: viewModel.purchaseCommunityPost ) ``` - [x] **GREEN 확인:** 오류 placeholder가 제거되고 상세 구매 연결·Debug build가 통과함을 확인한다. ```bash rg -n "showPurchaseError" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift rg -n "isShowPurchaseDialog|CommunityPostPurchaseDialog|func (presentPurchaseDialog|purchaseCommunityPost)|purchaseCommunityPost\(postId:" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 첫 `rg` match 0건·exit code 1, 둘째 `rg` 필수 연결 match, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** 목록 ViewModel에 구매 상태를 되돌리지 않고 상세의 기존 error/toast·refresh 흐름을 재사용한다. ### Phase 1 Gate **Goal 실행 `P1-GATE`:** 홈 계약과 목록 상호작용 변경의 컴파일·정적 계약을 판정한다. ```bash git diff --check rg -n "let (isCommentAvailable|isPinned): Bool" SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift rg -n "Color\.gray800" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift rg -n "CommunityPostPurchaseDialog|func purchaseCommunityPost|purchaseCommunityPost\(postId:" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: whitespace 오류 0건, 필드 2건, 세 UI의 `Color.gray800`·상세 구매 연결 match, `** BUILD SUCCEEDED **`. ## Phase 2: 홈 아이템 재사용과 메뉴 연결 **Phase 결과:** 홈이 기존 커뮤니티 리스트 아이템·상세 route·권한 메뉴를 재사용하고 변경 성공 후 홈·커뮤니티 목록을 동기화하며, 상세 유료 미구매 상태의 reaction·댓글 UI와 댓글 mutation을 차단한다. **선행조건:** `P1-GATE` 완료. **Phase 완료 조건:** `P2-T1`, `P2-T2`, `P2-R1`, `P2-R2`, `P2-R3`, `P2-R4`, `P2-GATE` 완료. ### Task 2.1 홈 섹션에 기존 리스트 아이템·상세·더보기 연결 **Goal 실행 `P2-T1`:** 홈 섹션이 최대 3개의 `CreatorChannelCommunityListItem`을 표시하고 본체는 상세로, 더보기는 기존 권한 메뉴로 연결한다. - **시작 조건:** `P1-GATE` 완료. - **완료 증거:** `CommunityPostCard` 사용 0건, 로컬 변환·`postId` route·홈 overlay 연결, Preview, Debug build. - **범위 밖:** 신규 표시 모델, 메뉴 UI 복제, 메인 홈 `CommunityPostCard` 수정. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` - Test: `CreatorChannelCommunitySection_Previews`·contract check·Debug build **Interfaces:** - Consumes: `CreatorChannelCommunityPostResponse`, `CreatorChannelCommunityPostItem`, `CreatorChannelCommunityViewModel.openReportMenu(post:)`, `AppStep.creatorChannelCommunityPostDetail`. - Produces: `CreatorChannelCommunitySection(isOwnCreatorChannel:onTapMore:onTapDetail:)`, `CreatorChannelHomeView(isOwnCreatorChannel:onTapCommunityMore:onTapCommunityDetail:)`, `showCommunityPostDetail(_ postId: Int)`. - [x] **RED:** 홈 섹션이 아직 `CommunityPostCard`를 사용하고 홈 상세·더보기 callback이 없음을 확인한다. ```bash rg -n "CommunityPostCard\(" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift rg -n "onTapCommunity(More|Detail)|communityPostItem" SodaLive/Sources/V2/CreatorChannel/Home SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift ``` Expected: 첫 명령 match 1건·exit code 0, 둘째 명령 match 0건·exit code 1. - [x] **RED 확인:** `communityOverlay` 노출이 `.community`에만 한정됨을 확인한다. ```bash rg -n "if viewModel\.selectedTab == \.community" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift ``` Expected: `communityOverlay` 함수 내 match 포함, exit code 0. - [x] **GREEN:** 홈 게시글을 커뮤니티 탭 아이템으로 연결하는 private property를 추가한다. ```swift private extension CreatorChannelCommunityPostResponse { var communityPostItem: CreatorChannelCommunityPostItem { CreatorChannelCommunityPostItem( postId: postId, creatorId: creatorId, creatorNickname: creatorNickname, creatorProfileUrl: creatorProfileUrl, createdAtUtc: dateUtc, content: content, imageUrl: imageUrl, audioUrl: audioUrl, price: price, isCommentAvailable: isCommentAvailable, likeCount: likeCount, commentCount: commentCount, isPinned: isPinned, existOrdered: existOrdered, isLiked: isLiked ) } } ``` `CreatorChannelCommunitySection`은 `isOwnCreatorChannel: Bool`, `onTapMore: (CreatorChannelCommunityPostItem) -> Void`, `onTapDetail: (Int) -> Void`를 받고 `CreatorChannelCommunityListItem`에 연결한다. `CreatorChannelHomeView`는 동일 상태·callback을 전달한다. 홈 섹션 Preview도 변경된 입력 계약을 명시한다. ```swift CreatorChannelCommunitySection( communities: previewCommunities, isOwnCreatorChannel: true, onSelectTab: { _ in }, onTapMore: { _ in }, onTapDetail: { _ in } ) ``` - [x] **GREEN:** 홈·커뮤니티 탭이 `postId` 기반 상세 helper를 공통 사용하고 홈에서도 기존 overlay를 보여준다. ```swift private func showCommunityPostDetail(_ postId: Int) { AppState.shared.setAppStep( step: .creatorChannelCommunityPostDetail( postId: postId, onCommunityRefresh: { viewModel.fetchHome(creatorId: creatorId) communityViewModel.fetchFirstPage(creatorId: creatorId) } ) ) } ``` `CreatorChannelCommunityTabView.onTapDetail`도 `(Int) -> Void`로 줄이고 `post.postId`를 전달한다. `communityOverlay` 조건은 `.home || .community`로 확장한다. - [x] **GREEN 확인:** 기존 카드·탭 전용 상세 모델 callback이 제거되고 새 연결과 Debug build가 통과함을 확인한다. ```bash rg -n "CommunityPostCard\(" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift rg -n "communityPostItem|CreatorChannelCommunityListItem\(|onTapCommunityMore|onTapCommunityDetail|showCommunityPostDetail\(_ postId: Int\)" SodaLive/Sources/V2/CreatorChannel/Home SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 첫 명령 match 0건·exit code 1, 둘째 명령 필수 연결 match, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** `CreatorChannelCommunitySection.relativeTimeText` 같은 카드 제거로 미사용된 헬퍼만 제거하고, `CommunityPostCard`의 다른 사용처는 건드리지 않는다. ### Task 2.2 관리 동작 성공 후 홈·커뮤니티 동기화 **Goal 실행 `P2-T2`:** 홈 더보기에서 고정·수정·삭제한 결과가 홈과 커뮤니티 탭 목록에 모두 반영된다. - **시작 조건:** `P2-T1` 완료. - **완료 증거:** 한 개의 `refreshCommunityFeeds()` helper, 변경 성공 callback, Debug build. - **범위 밖:** 좋아요 optimistic state, 신고 성공 후 목록 재조회, 메뉴 UI 변경. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` - Test: contract check·Debug build·시뮬레이터 관리 액션 확인 **Interfaces:** - Consumes: `deleteCommunityPost`, `updateCommunityPostFixed`, `showCommunityModify`, 두 ViewModel의 재조회 메서드. - Produces: `deleteCommunityPost(onSuccess:)`, `updateCommunityPostFixed(onSuccess:)`, `refreshCommunityFeeds()`. - [x] **RED:** 삭제·고정·수정 성공 후 커뮤니티 탭만 재조회하는 현재 구조를 확인한다. ```bash rg -n "fetchFirstPage\(creatorId: creatorId\)|fetchHome\(creatorId: creatorId\)|deleteCommunityPost\(creatorId:|updateCommunityPostFixed\(creatorId:" SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift ``` Expected: ViewModel 내 재조회와 `creatorId` 기반 mutation, 수정 callback의 커뮤니티 탭 재조회 match. - [x] **RED 확인:** `refreshCommunityFeeds` helper가 없음을 확인한다. ```bash rg -n "refreshCommunityFeeds" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift ``` Expected: match 0건, exit code 1. - [x] **GREEN:** ViewModel mutation은 성공 시 주입된 callback을 호출하고 재조회 소유권을 `CreatorChannelView`로 옮긴다. ```swift func deleteCommunityPost(onSuccess: @escaping () -> Void) func updateCommunityPostFixed(onSuccess: @escaping () -> Void) private func refreshCommunityFeeds() { viewModel.fetchHome(creatorId: creatorId) communityViewModel.fetchFirstPage(creatorId: creatorId) } ``` 두 mutation의 기존 성공 분기에서 ViewModel 자체 재조회 대신 callback을 실행한다. ```swift if decoded.success { self.errorMessage = I18n.Explorer.deleted self.isShowPopup = true onSuccess() } ``` ```swift if decoded.success { onSuccess() } ``` `fixedAction`, 삭제 확인 action, `showCommunityModify` 성공 callback이 `refreshCommunityFeeds` 하나를 사용하도록 연결한다. - [x] **GREEN 확인:** helper가 두 재조회를 포함하고 mutation의 기존 `creatorId` 인자·ViewModel 내 재조회가 제거됨을 확인한다. ```bash rg -n "refreshCommunityFeeds|fetchHome\(creatorId: creatorId\)|fetchFirstPage\(creatorId: creatorId\)" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift rg -n "deleteCommunityPost\(creatorId:|updateCommunityPostFixed\(creatorId:|fetchFirstPage\(creatorId: creatorId\)" SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 첫 명령은 helper·두 재조회·세 action 연결 match, 둘째 명령 match 0건·exit code 1, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** 기존 글 작성 성공 callback은 요청 범위가 아니므로 별도 정리하지 않고 이번 변경의 중복만 제거한다. ### Regression Task 2.3 표시할 reaction이 없을 때 빈 공간 제거 **Goal 실행 `P2-R1`:** 홈·커뮤니티 리스트와 상세의 유료 미구매 상태에서 하트·댓글과 reaction bar의 고정 높이를 함께 제거한다. - **리뷰 근거:** `REV-P3-003` — `reviews/phase3-community-item-unification.md` - **시작 조건:** 사용자 회귀 제보와 코드 근거 확인, PRD `CCCI-009`·`DEC-013` 반영. - **완료 증거:** 두 View의 무조건 `reactionBar` 삽입 0건, `if !isPaidLocked` 조건부 삽입 2건, `git diff --check`, Debug build. - **범위 밖:** reaction icon·count 스타일 변경, 썸네일 레이아웃 변경, 신규 helper·test target 추가. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift` - Test: 신규 test 파일 없음—focused contract check·Debug build 사용 **Interfaces:** - Consumes: 각 View의 기존 `isPaidLocked`, `reactionBar`. - Produces: 유료 잠금 상태에서 `reactionBar`가 View 계층에 없는 레이아웃. - [x] **RED:** 두 View body가 `reactionBar`를 무조건 삽입하고, 내부의 빈 `HStack`이 `.frame(height: 24)`를 유지함을 확인한다. ```bash rg -n '^[[:space:]]{12}reactionBar$' SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift rg -n 'frame\(height: 24\)' SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift ``` Expected: 첫 명령 match 2건, 둘째 명령 match 2건. - [x] **GREEN:** 두 body에서 `reactionBar`를 `if !isPaidLocked`일 때만 삽입하고, 내부의 중복 잠금 조건만 제거한다. - [x] **GREEN 확인:** 무조건 삽입이 사라지고 조건부 삽입·Debug build가 통과함을 확인한다. ```bash rg -n '^[[:space:]]{12}reactionBar$' SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift rg -n -U 'if !isPaidLocked \{\n[[:space:]]+reactionBar' SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift git diff --check xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 첫 명령 match 0건·exit code 1, 둘째 명령 match 2건, whitespace 오류 0건, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** 신규 상태·helper를 추가하지 않고 기존 `isPaidLocked`만 재사용한다. ### Regression Task 2.4 공지 구매를 상세 흐름으로 일원화 **Goal 실행 `P2-R2`:** 홈 공지의 본체·댓글·유료 잠금 영역이 기존 커뮤니티 상세로 이동하고 구매는 상세에서만 시작한다. - **리뷰 근거:** `REV-P3-004` — `reviews/phase3-community-item-unification.md` - **시작 조건:** 사용자 후속 요청과 코드 근거 확인, PRD `CCCI-010`·`DEC-014` 반영. - **완료 증거:** 공지 구매 callback 0건, 단일 상세 callback과 홈 `onTapCommunityDetail` 연결, `git diff --check`, Debug build. - **범위 밖:** 공지 카드 외형·오디오 재생 동작, 구매 endpoint·dialog 내부 UI, 신규 helper·test target 추가. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` - Test: 신규 test 파일 없음—focused contract check·Debug build 사용 **Interfaces:** - Consumes: `CreatorChannelHomeView.onTapCommunityDetail`, `CreatorChannelView.showCommunityPostDetail(_:)`, 상세 구매·refresh 흐름. - Produces: 공지 카드의 모든 게시글 진입점이 같은 `postId` 상세 route를 사용하는 동작. - [x] **RED:** 공지에 별도 `onTapPurchase`·`onTapNotice`·`onTapComment`가 남고 홈 조립부가 상세 callback을 전달하지 않음을 확인한다. ```bash rg -n "onTapPurchase|onTapNotice|onTapComment|Button\(action: onTapPurchase\)" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift rg -n "CreatorChannelNoticeSection\(notices: response\.notices\)" SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift ``` Expected: 두 명령 모두 match·exit code 0. - [x] **RED 확인:** 상세 route와 상세 구매 성공 후 상세·홈·커뮤니티 refresh 흐름은 이미 존재함을 확인한다. - [x] **GREEN:** 공지의 세 callback을 단일 `onTapDetail`로 교체하고 잠금 구매 `Button` wrapper를 제거한 뒤 홈의 `onTapCommunityDetail`을 전달한다. - [x] **GREEN 확인:** 기존 공지 callback 0건, 단일 상세 callback·홈 연결 match, `git diff --check`, Debug build가 통과함을 확인한다. ```bash rg -n "onTapPurchase|onTapNotice|onTapComment|Button\(action: onTapPurchase\)" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift rg -n "onTapDetail|onTapCommunityDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift git diff --check xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 첫 명령 match 0건·exit code 1, 둘째 명령 필수 연결 match, whitespace 오류 0건, `** BUILD SUCCEEDED **`. - [x] **REFACTOR:** 공지 카드 외형·오디오 재생과 기존 상세·구매·refresh 구현은 변경하지 않는다. ### Follow-up Task 2.5 상세 유료 미구매 댓글 영역·동작 차단 **Goal 실행 `P2-R3`:** 커뮤니티 상세의 유료 미구매 잠금 상태에서 댓글 UI를 표시하지 않고 작성·수정·삭제 mutation을 실행하지 않는다. - **시작 조건:** 사용자 후속 요청·인터뷰 A안 확정, PRD `CCCI-011`·`DEC-015` 반영. - **완료 증거:** 상세 댓글 구분선·목록·입력창의 단일 조건부 삽입, ViewModel 댓글 mutation guard, focused contract check, `git diff --check`, Debug build, 상태별 수동 확인. - **범위 밖:** 기존 reaction bar 구현 재수정, 상세 API 댓글 payload 변경, `CreatorChannelReplyDetail/**`, 신규 공용 모델·test target 추가. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift` - Test: 신규 test 파일 없음—focused contract check·Debug build·시뮬레이터 사용 **Interfaces:** - Consumes: `detail.price`, `detail.existOrdered`, `detail.isCommentAvailable`, 기존 `isOwnPost`, 댓글 전송·삭제 진입점. - Produces: 유료 잠금과 댓글 허용 상태를 함께 판정하는 단일 ViewModel 상태, 잠금 상태에서 댓글 UI·mutation이 없는 상세 화면. - [x] **RED:** 상세 View가 유료 잠금 여부 없이 `isCommentAvailable`만으로 댓글 목록·입력창을 표시하고 ViewModel의 댓글 전송 가능 여부도 같은 필드만 확인함을 재현한다. ```bash rg -n "if detail\.isCommentAvailable|detail\?\.isCommentAvailable == true" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift rg -n "guard detail\?\.isCommentAvailable == true|func sendComment|func confirmDelete" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift ``` Expected: 상세 View의 댓글 목록·입력창 조건과 ViewModel의 기존 전송 조건이 match. - [x] **GREEN:** ViewModel에 기존 유료 잠금 조건과 `isCommentAvailable`을 결합한 단일 댓글 상호작용 상태를 추가한다. - [x] **GREEN:** 상세 View의 댓글 구분선·목록·하단 입력창은 해당 상태가 `true`일 때만 삽입한다. - [x] **GREEN:** `sendComment`와 `confirmDelete`가 같은 상태를 guard하고, `applyDetail`은 잠금 상태의 댓글·편집·삭제 임시 상태를 기존 댓글 비활성 분기와 동일하게 비운다. - [x] **GREEN 확인:** 댓글 표시 조건과 mutation guard가 단일 상태를 사용하고 기존 직접 조건이 상세 View에서 제거됐는지 확인한다. ```bash rg -n "isCommentInteractionAvailable" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift rg -n "if detail\.isCommentAvailable|detail\?\.isCommentAvailable == true" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift rg -n "guard isCommentInteractionAvailable" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift git diff --check xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: 단일 상태가 View·ViewModel에서 match, 상세 View의 기존 직접 조건 0건·exit code 1, mutation guard match, whitespace 오류 0건, `** BUILD SUCCEEDED **`. - [x] **수동 확인:** 타인 게시글의 유료 미구매 상태에서는 reaction·댓글 구분선·목록·입력창이 보이지 않고 댓글 요청이 발생하지 않는다. - [x] **수동 확인:** 무료·구매 완료·본인 게시글에서는 기존 `isCommentAvailable` 값에 따라 댓글 UI와 동작이 유지된다. - [x] **REFACTOR:** 기존 상세 응답·repository·답글 화면과 이미 완료된 reaction bar 구현은 변경하지 않는다. ### Follow-up Task 2.6 유료 미구매 타인 게시글 더보기 숨김 **Goal 실행 `P2-R4`:** 홈·커뮤니티 탭 리스트 아이템의 더보기를 유료 미구매 타인 게시글에서 숨기고 무료·구매 완료·작성자 게시글에서 표시한다. - **시작 조건:** 사용자 상태별 수동 검증과 후속 노출 규칙 확정, PRD `CCCI-012`·`DEC-016` 반영. - **완료 증거:** 공용 리스트 아이템 조건의 RED/GREEN contract check, `git diff --check`, Debug build, 상태별 수동 확인. - **범위 밖:** grid·공지 카드, 메뉴 내용·권한, 가격 표시, 상세·좋아요 gesture 변경. **Files:** - Modify: `SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift` - Test: 신규 test 파일 없음—focused contract check·Debug build·수동 확인 사용 **Interfaces:** - Consumes: 기존 `isPaidLocked`, `isOwnPost`, `post.price`, `post.existOrdered`. - Produces: 유료 미구매 타인 게시글에는 더보기가 없고 나머지 상태에는 기존 더보기 callback이 유지되는 header. - [x] **RED:** 방문자 분기가 구매 상태와 무관하게 `else if !isOwnPost`로 더보기를 삽입함을 확인한다. - [x] **GREEN:** 방문자 분기를 `else if !isPaidLocked`로 변경해 기존 잠금 판정을 재사용한다. - [x] **GREEN 확인:** 기존 조건 0건, 신규 조건 1건, `git diff --check`, Debug build를 확인한다. - [ ] **수동 확인:** 홈·커뮤니티 탭에서 유료 미구매 타인은 숨김, 무료·구매 완료·작성자는 표시된다. - [x] **REFACTOR:** 신규 상태·helper 없이 조건 한 줄만 변경하고 grid·공지·메뉴 callback은 수정하지 않는다. ### Phase 2 Gate **Goal 실행 `P2-GATE`:** 홈 재사용·상세·더보기·목록 동기화 구조를 판정한다. ```bash git diff --check rg -n "communityPostItem|CreatorChannelCommunityListItem\(|onTapCommunityMore|onTapCommunityDetail|refreshCommunityFeeds" SodaLive/Sources/V2/CreatorChannel/Home SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift rg -n "CommunityPostCard\(" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift rg -n -U 'if !isPaidLocked \{\n[[:space:]]+reactionBar' SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift rg -n "onTapPurchase|onTapNotice|onTapComment" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift rg -n "onTapDetail|onTapCommunityDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift rg -n "isCommentInteractionAvailable|guard isCommentInteractionAvailable" SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailView.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/CreatorChannelCommunityPostDetailViewModel.swift rg -n "else if !isPaidLocked" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: whitespace 오류 0건, 필수 연결·상세 댓글 조건/guard match, 홈 `CommunityPostCard`·공지 기존 callback match 0건·exit code 1, `** BUILD SUCCEEDED **`. ## Phase 3: 리뷰·통합 검증·문서 종결 **Phase 결과:** 정적 계약·Debug build·Preview·시뮬레이터 흐름을 확인하고 PRD·계획·리뷰 기록을 동기화한다. **선행조건:** `P2-R3`와 재개한 `P2-GATE` 완료. 시뮬레이터 통합 판정은 `EXT-001` 반영 후 실행. **Phase 완료 조건:** `P3-T1`, `P3-GATE` 완료·검증 기록 누적. ### Task 3.1 영향 범위 리뷰와 완료 증거 누적 **Goal 실행 `P3-T1`:** 확정 요구사항 전체의 자동·수동 증거와 잔여 위험을 문서화한다. - **시작 조건:** `P2-GATE` 완료. - **완료 증거:** 리뷰 리포트, 정적 검사, Debug build, Preview/시뮬레이터 결과, PRD·계획 Progress. - **범위 밖:** 실패와 무관한 리팩터링, test target 추가, 백엔드 수정. - **TDD 예외 사유:** production 동작을 추가하지 않는 리뷰·검증·문서 Task다. - **대체 검증 방법:** 아래 정적 검사·Debug build·Preview·시뮬레이터 대조표를 사용한다. **Files:** - Create: `docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/reviews/phase3-community-item-unification.md` - Modify: `docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md` - Modify: `docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/plan-task.md` - Test: 정적 검사·Debug build·Preview·시뮬레이터 대체 검증 **Interfaces:** - Consumes: `CCCI-001~012`, `EXT-001`, Phase 1·2 산출물. - Produces: Phase 3 리뷰 판정, 최종 Progress, 남은 외부 조건. - [x] 정적 검사로 필수 연결과 금지 호출처를 확인한다. ```bash rg -n "let (isCommentAvailable|isPinned): Bool" SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift rg -n "communityPostItem|CreatorChannelCommunityListItem\(|showCommunityPostDetail\(_ postId: Int\)|refreshCommunityFeeds" SodaLive/Sources/V2/CreatorChannel/Home SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift rg -n "onTapPurchase|openPurchaseDialog|isShowPostPurchaseView|selectedPostPrice|purchaseCommunityPost" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityTabView.swift SodaLive/Sources/V2/CreatorChannel/Community/CreatorChannelCommunityViewModel.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift rg -n "onTapPurchase|onTapNotice|onTapComment" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift rg -n "onTapDetail|onTapCommunityDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift rg -n "Color\.gray800" SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityListItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Components/CreatorChannelCommunityGridItem.swift SodaLive/Sources/V2/CreatorChannel/Community/Detail/Components/CreatorChannelCommunityPostDetailContentView.swift git diff --check ``` Expected: 필드·홈 연결·세 UI의 `Color.gray800` match, 목록 구매 심볼 0건·exit code 1, whitespace 오류 0건. - [x] `SodaLive-dev` Debug build를 실행한다. ```bash xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` Expected: exit code 0, `** BUILD SUCCEEDED **`. - [ ] Preview에서 홈 유료 잠금·고정·댓글 비활성 상태와 썸네일 유료 잠금 상태를 확인한다. - [ ] `EXT-001` 반영 후 시뮬레이터에서 홈·리스트형·썸네일형 본체 터치가 상세로 이동하고 목록 구매 dialog가 나오지 않음을 확인한다. - [ ] 본인 채널의 고정·수정·삭제, 타인 채널의 신고, 상세 구매, 더보기·좋아요 터치 중복 없음을 수동 확인한다. - [ ] 상세의 유료 미구매 상태에서 reaction·댓글 UI 미노출과 댓글 mutation 차단을 확인하고, 구매 후 `isCommentAvailable`에 따른 복원을 확인한다. - [x] `docs/sample/sample-review.md`를 읽고 `phase3-community-item-unification.md`에 검토 범위·근거·명령·발견 사항을 기록한다. - [x] 확정 수정 항목이 있으면 코드 수정 전 해당 Phase에 회귀 Task를 추가한다. - [x] PRD 상태·요구사항 추적표와 이 문서의 체크박스·Progress를 실제 결과로 갱신한다. ### Phase 3 Gate **Goal 실행 `P3-GATE`:** 확정 요구사항·자동 검증·수동 검증·문서 추적성을 최종 판정한다. - [x] `P3-T1` 리뷰에 확정 수정 항목이 없거나 추가 회귀 Task가 완료됐다. - [x] `git diff --check`·정적 contract check·`SodaLive-dev` Debug build가 통과했다. - [ ] Preview 검증이 통과했다. - [ ] `EXT-001` 반영 후 시뮬레이터 통합 검증이 통과했다. 미반영이면 Phase를 완료 표시하지 않고 재개 조건을 Progress에 남긴다. - [x] PRD·`plan-task.md`·review 리포트의 판정과 검증 기록이 일치한다. ## 실행 순서와 의존성 | 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 | |---:|---|---|---|---| | 1 | `P1-T1` | PRD·`EXT-001` 계약 확정 | 아니요 | 백엔드 필드명·타입 확인 | | 2 | `P1-T2` | `P1-T1` 완료 | 아니요 | 목록 호출처·상세 구매 경계 재확인 | | 3 | `P1-T3` | `P1-T2` 완료 | 아니요 | 기존 상세 잠금·구매 API 경계 재확인 | | 4 | `P1-GATE` | Phase 1 Task 전체 | 아니요 | 실패 소유 Task 수정 | | 5 | `P2-T1` | `P1-GATE` | 아니요 | 홈·탭 모델 매핑 재확인 | | 6 | `P2-T2` | `P2-T1` | 아니요 | mutation 성공 callback 호출처 재확인 | | 7 | `P2-R1` | 사용자 회귀 제보·PRD 반영 | 아니요 | reaction bar 삽입 조건 재확인 | | 8 | `P2-R2` | 사용자 후속 요청·PRD 반영 | 아니요 | 공지 상세 callback 경계 재확인 | | 9 | `P2-R3` | 사용자 후속 요청·인터뷰·PRD 반영 | 아니요 | 상세 댓글 표시·mutation 진입점 재확인 | | 10 | `P2-R4` | 사용자 상태별 수동 검증·PRD 반영 | 아니요 | 공용 리스트 아이템 더보기 조건 재확인 | | 11 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task 수정 | | 12 | `P3-T1` | `P2-GATE` | 아니요 | 리뷰 수정 Task 추가 | | 13 | `P3-GATE` | `P3-T1`·`EXT-001` | 아니요 | 백엔드 반영 후 통합 검증 재개 | ```text P1-T1 → P1-T2 → P1-T3 → P1-GATE → P2-T1 → P2-T2 → P2-R1 → P2-R2 → P2-R3 → P2-R4 → P2-GATE → P3-T1 → P3-GATE ``` ## 변경 금지 항목 - `CommunityPostCard`와 메인 홈 팔로잉·추천 사용처를 수정하지 않는다. - 구매 endpoint와 `CommunityPostPurchaseDialog` 내부 UI를 변경하지 않는다. - 홈 API의 `dateUtc`를 임의로 rename·parse·timezone 변환하지 않는다. - 백엔드 미반영 필드에 optional·fallback 값을 만들어 통합 실패를 숨기지 않는다. - 신규 protocol·factory·display model·dependency·test target을 추가하지 않는다. - 상세 API의 댓글 payload 계약과 `CreatorChannelReplyDetail/**`를 변경하지 않는다. - 요청 범위 밖의 기존 dead code·format·naming을 정리하지 않는다. ## 의사결정 및 중단 규칙 - PRD·현재 API 응답·구현이 충돌하면 PRD의 `EXT-001`을 우선하고 추정 구현하지 않는다. - iOS 컴파일과 Preview 작업은 백엔드 반영 전에 진행할 수 있지만, 시뮬레이터 통합 판정은 `EXT-001` 반영 후에만 완료한다. - 구현 범위가 바뀌면 PRD Decision Log·요구사항·이 문서 체크박스를 코드 수정 전에 갱신한다. - 같은 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고 독립 작업도 불가능할 때만 차단으로 판정한다. - 체크박스·검증·Progress가 모두 충족되기 전에 Goal·Phase를 완료 표시하지 않는다. ## Progress 기존 기록을 삭제하지 않고 실제 실행 결과를 누적한다. ### 계획 작성 — 2026-08-10 - 상태: 계획 검토 대기 - 무엇을: 인터뷰·설계 승인 결과를 Phase 1~3·Task 6개·Gate 3개로 변환했고, 기준선 확인에서 누락된 상세 구매 이동 Task를 추가했다. - 왜: 코드 수정 전 파일 범위·인터페이스·검증·외부 의존을 확정하기 위해서다. - 어떻게: - `rg`로 현재 모델·callback·구매 상태·색상·Preview fixture 호출처를 확인했다. - `docs/agent-guides/documentation-policy.md`, `build-test-verification.md`, `code-style.md`, `agent-execution-policy.md`와 현재 PRD·샘플을 대조했다. - 남은 항목: 사용자 계획 검토, `P1-T1` 시작. - 다음 행동: 계획 승인 후 `superpowers:executing-plans`로 `P1-T1`을 실행한다. ### Phase 1~2 구현 — 2026-08-10 - 상태: 코드·정적 계약·Debug build 완료 - 무엇을: - 홈 응답에 `isCommentAvailable`·`isPinned`을 추가했다. - 목록 구매를 제거하고 잠금 배경을 `Color.gray800`으로 통일했다. - 기준선에서 발견한 상세 구매 누락을 `P1-T3`로 추가한 뒤 기존 dialog·API를 상세로 이동했다. - 홈을 `CreatorChannelCommunityListItem`으로 교체하고 상세·더보기·두 목록 refresh를 연결했다. - 왜: `CCCI-001~008`의 iOS 코드 범위를 충족하기 위해서다. - 어떻게: - 각 Task의 RED/GREEN `rg` contract check를 실행했다. - iPhone 17 Pro iOS 26.0 대상 `SodaLive-dev` Debug build를 실행해 exit code 0을 확인했다. - 남은 항목: Preview·Simulator 통합 검증. - 다음 행동: Phase 3 리뷰와 외부 조건 확인. ### Phase 3 리뷰 — 2026-08-10 - 상태: 코드 리뷰 완료·통합 검증 대기 - 무엇을: 전체 diff를 PRD·계획과 대조하고 `reviews/phase3-community-item-unification.md`를 작성했다. - 왜: 완료 주장을 코드·빌드·수동 증거로 분리하기 위해서다. - 어떻게: - 정적 계약·`git diff --check`·Debug build는 통과했다. - `xcrun simctl list devices` 결과 사용 가능한 Simulator가 모두 `Shutdown`임을 확인했다. - 남은 항목: `EXT-001` 반영, Preview, 본체 상세 이동·권한별 더보기·목록 dialog 미노출·상세 구매 수동 확인. - 다음 행동: 사용자가 Simulator를 부팅하고 백엔드 필드 반영을 알리면 `P3-T1`·`P3-GATE`를 재개한다. ### Reaction bar 회귀 수정 — 2026-08-10 - 상태: `P2-R1` 구현 전 - 무엇을: 홈·커뮤니티 리스트와 상세의 하트·댓글이 모두 숨겨질 때 남는 빈 공간을 회귀 Task로 추가했다. - 왜: 두 View가 빈 reaction `HStack`의 고정 24pt 높이를 계속 배치하는 근본 원인을 확인했다. - 어떻게: 공용 리스트 아이템과 상세 content View의 body·`reactionBar`를 추적하고 `REV-P3-003`, `CCCI-009`, `DEC-013`을 연결했다. - 남은 항목: RED 확인, 최소 코드 수정, 정적 검사·Debug build, 문서 결과 누적. - 다음 행동: `P2-R1`을 실행한다. ### Reaction bar 회귀 수정 완료 — 2026-08-10 - 상태: `P2-R1`·`P2-GATE` 완료 - 무엇을: 공용 리스트 아이템과 상세에서 유료 잠금 상태일 때 `reactionBar` 전체를 View 계층에서 제외했다. - 왜: 하트·댓글 자식만 숨기면 빈 `HStack`의 24pt 높이와 부모 spacing이 남기 때문이다. - 어떻게: - RED: body의 무조건 `reactionBar` 삽입 2건, `.frame(height: 24)` 2건을 확인했다. - GREEN: 무조건 삽입 0건, `if !isPaidLocked` 조건부 삽입 2건을 확인했다. - `git diff --check` 오류 0건, iPhone 17 Pro 대상 `SodaLive-dev` Debug build exit code 0을 확인했다. - 남은 항목: 기존 `EXT-001`·부팅된 Simulator 기반 Preview/통합 검증. - 다음 행동: 외부 조건 충족 후 `P3-GATE`를 재개한다. ### 공지 상세 구매 일원화 — 2026-08-10 - 상태: `P2-R2` 구현·정적 계약·Debug build 완료 - 무엇을: 홈 공지의 별도 구매 callback과 no-op 상세·댓글 callback을 하나의 상세 이동 흐름으로 통일한다. - 왜: 공지도 커뮤니티 게시글이므로 목록에서 구매하지 않고 상세에서만 구매해야 한다. - 어떻게: `REV-P3-004`, `CCCI-010`, `DEC-014`를 연결하고 focused RED/GREEN contract check와 Debug build를 완료 증거로 사용한다. - 결과: - RED: 공지 직접 구매 callback 7건과 상세 callback 미전달 생성부 1건을 확인했다. - GREEN: 기존 공지 callback 0건, 단일 `onTapDetail`과 홈 `onTapCommunityDetail` 연결을 확인했다. - `git diff --check` 오류 0건, `SodaLive-dev` Debug build exit code 0을 확인했다. - 남은 항목: 기존 `EXT-001`·부팅된 Simulator 기반 통합 검증. - 다음 행동: 외부 조건 충족 후 `P3-GATE`를 재개한다. ### 상세 유료 미구매 댓글 차단 계획 — 2026-08-10 - 상태: `P2-R3` 계획 확정·구현 대기 - 무엇을: 상세의 유료 미구매 상태에서 기존 reaction bar 차단을 유지하고 댓글 구분선·목록·입력창과 작성·수정·삭제 mutation을 차단하는 후속 Task를 추가했다. - 왜: 현재 상세 View와 ViewModel은 `isCommentAvailable`만 확인해 유료 미구매 방문자도 댓글을 보고 mutation을 실행할 수 있기 때문이다. - 어떻게: - 사용자 인터뷰에서 UI와 ViewModel 동작을 모두 차단하는 A안을 확정했다. - 기존 유료 잠금 조건과 댓글 허용 값을 결합한 단일 ViewModel 상태를 View 표시 조건·mutation guard에 재사용하도록 `P2-R3`에 기록했다. - 이미 완료된 reaction bar 차단은 `P2-R1`을 유지하고 중복 Task를 만들지 않았다. - 남은 항목: `P2-R3` 구현·focused contract check·Debug build·상태별 수동 확인. - 다음 행동: 사용자가 별도로 구현을 요청할 때 `P2-R3`부터 실행한다. 현재 요청에 따라 코드 수정·커밋은 진행하지 않는다. ### 상세 유료 미구매 댓글 차단 구현 — 2026-08-10 - 상태: `P2-R3` 코드·정적 계약·device/Simulator Debug build·상태별 수동 검증 완료 - 무엇을: `isCommentInteractionAvailable` 하나로 상세 댓글 구분선·목록·입력창과 페이지네이션·작성·수정·삭제·상태 초기화를 통일했다. - 왜: `isCommentAvailable`만으로는 타인 유료 미구매 게시글의 댓글 접근과 mutation을 차단하지 못하기 때문이다. - 어떻게: - RED: 상세 View 직접 조건 2건과 ViewModel의 paid-lock 미반영 guard를 확인했다. - GREEN: 상세 View 직접 조건 0건, shared 상태 참조 10건, mutation guard match, `git diff --check` 성공을 확인했다. - `SodaLive-dev` device Debug build와 iPhone 17 Pro iOS 26.0 Simulator Debug build가 각각 exit code 0으로 성공했다. - Simulator에 앱을 설치·실행해 홈 진입을 캡처한 뒤 종료했다. - 수동 검증: 사용자가 유료 미구매·무료·구매 완료·본인 게시글의 댓글 UI·요청 동작을 확인했다. - 남은 항목: `EXT-001` 통합 검증. - 다음 행동: 후속 `P2-R4`를 실행한다. ### 유료 미구매 타인 게시글 더보기 숨김 계획 — 2026-08-10 - 상태: `P2-R4` 계획 확정·구현 대기 - 무엇을: 홈·커뮤니티 탭의 공용 리스트 아이템에서 유료 미구매 타인의 더보기를 숨기고 무료·구매 완료·작성자에게 표시한다. - 왜: 현재 방문자 분기 `else if !isOwnPost`가 구매 상태를 확인하지 않아 유료 미구매 게시글에도 더보기를 표시한다. - 어떻게: 기존 `isPaidLocked`를 재사용해 방문자 분기를 `else if !isPaidLocked`로 교체하는 한 줄 변경과 focused RED/GREEN 검증을 `P2-R4`에 기록했다. - 남은 항목: 코드 수정·정적 검사·Debug build·상태별 확인. - 다음 행동: `P2-R4` RED 확인 후 최소 구현한다. ### 유료 미구매 타인 게시글 더보기 숨김 구현 — 2026-08-10 - 상태: `P2-R4` 코드·정적 계약·Debug build 완료, 상태별 수동 확인 대기 - 무엇을: 공용 `CreatorChannelCommunityListItem`의 방문자 더보기 조건을 `!isOwnPost`에서 `!isPaidLocked`로 변경했다. - 왜: 홈·커뮤니티 리스트의 유료 미구매 타인 게시글에서 더보기를 제거하고 무료·구매 완료·작성자 게시글에는 유지하기 위해서다. - 어떻게: - RED: 기존 조건 1건·신규 조건 0건을 확인했다. - GREEN: 기존 조건 0건·신규 조건 1건, production diff 한 줄, `git diff --check` 성공을 확인했다. - `SodaLive-dev` Debug build가 exit code 0으로 성공했다. - 남은 항목: 홈·커뮤니티 탭의 네 가지 상태별 수동 확인, `EXT-001` 통합 검증. - 다음 행동: 상태별 수동 확인 후 `P2-R4`와 `P2-GATE`를 완료 판정한다. ## Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | |---|---|---|---|---|---| | 2026-08-10 | `PLAN-DEC-001` | 확정 | 로컬 변환·기존 컴포넌트·기존 메뉴 상태를 재사용한다. | PRD `DEC-008~011` | `P2-T1`, `P2-T2` | | 2026-08-10 | `PLAN-DEC-002` | 확정 | 목록 구매를 홈·리스트형·썸네일형에서 모두 제거한다. | PRD `DEC-003`, `DEC-007` | `P1-T2` | | 2026-08-10 | `PLAN-DEC-003` | 확정 | 백엔드 필드는 non-optional로 디코딩하고 미반영 응답을 fallback으로 숨기지 않는다. | PRD `DEC-006`, `EXT-001` | `P1-T1`, `P3-GATE` | | 2026-08-10 | `PLAN-DEC-004` | 확정 | 신규 test target 대신 contract check·Preview·Debug build·시뮬레이터를 완료 증거로 사용한다. | 현재 프로젝트에 unit test bundle 없음 | 전체 Task·Gate | | 2026-08-10 | `PLAN-DEC-005` | 확정 | 목록의 기존 구매 상태·API 호출을 상세 View·ViewModel로 옮긴다. | 상세의 현재 `showPurchaseError()`만으로는 사용자 요구인 상세 구매가 불가능함 | `P1-T2`, `P1-T3` | | 2026-08-10 | `PLAN-DEC-006` | 확정 | 하트·댓글이 모두 숨겨지는 경우 높이 보정 대신 reaction bar 전체를 조건부로 제외한다. | 공용 리스트·상세의 빈 고정 높이를 같은 기존 상태로 제거하는 최소 변경 | `P2-R1`, `CCCI-009` | | 2026-08-10 | `PLAN-DEC-007` | 확정 | 공지 카드의 별도 구매·상세·댓글 callback을 단일 상세 callback으로 교체하고 기존 홈 상세 route를 재사용한다. | 공지도 커뮤니티 게시글이며 상세 전용 구매 원칙을 동일하게 적용 | `P2-R2`, `CCCI-010` | | 2026-08-10 | `PLAN-DEC-008` | 확정 | 상세 View와 ViewModel은 기존 유료 잠금 조건과 `isCommentAvailable`을 결합한 단일 상태로 댓글 UI·작성·수정·삭제 mutation을 함께 차단한다. | 사용자 인터뷰 A안·PRD `DEC-015` | `P2-R3`, `CCCI-011` | | 2026-08-10 | `PLAN-DEC-009` | 확정 | 공용 리스트 아이템의 방문자 더보기 분기를 기존 `isPaidLocked`의 반대로 제한한다. | 신규 helper 없이 홈·커뮤니티 리스트에 동시에 적용되는 최소 변경 | `P2-R4`, `CCCI-012` | ## 발견된 문제 | ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 | |---|---|---|---|---|---| | `ISSUE-001` | High | 외부 조건 대기 | 백엔드 홈 API 응답에 `isCommentAvailable`·`isPinned`이 없으면 신규 iOS 모델 디코딩에 실패한다. | `P1-T1`, `P3-GATE` | iOS 구현·빌드는 완료했고 `EXT-001` 반영 후 통합 검증한다. | | `ISSUE-002` | High | 환경 대기 | 사용 가능한 Simulator가 모두 `Shutdown`이라 실제 터치·메뉴·구매 검증을 실행하지 못했다. | `P3-T1`, `P3-GATE` | Simulator 부팅 후 수동 검증을 재개한다. | | `ISSUE-003` | Medium | 수정 완료 | 유료 잠금 상태에서 하트·댓글은 숨겨지지만 빈 reaction bar의 고정 24pt 높이가 홈·목록·상세에 남는다. | `P2-R1`, `P3-GATE` | 조건부 삽입 contract check·Debug build 통과. | | `ISSUE-004` | High | 수정 완료 | 홈 공지는 별도 구매 callback이 남고 실제 조립부가 상세 callback을 전달하지 않아 상세 전용 구매 원칙을 충족하지 않는다. | `P2-R2`, `P3-GATE` | 단일 상세 callback 연결·직접 구매 callback 제거·Debug build 통과. | | `ISSUE-005` | High | 수정·수동 검증 완료 | 상세가 `isCommentAvailable`만으로 댓글 UI·전송 가능 여부를 판정해 유료 미구매 상태에서도 댓글을 조회하고 mutation을 실행할 수 있다. | `P2-R3`, `P2-GATE`, `P3-GATE` | 단일 댓글 상호작용 상태와 mutation guard를 적용하고 사용자가 상태별 동작을 확인했다. | | `ISSUE-006` | Medium | 코드·자동 검증 완료·수동 대기 | 공용 리스트 아이템이 타인 게시글이면 구매 상태와 무관하게 더보기를 표시한다. | `P2-R4`, `P2-GATE` | 방문자 분기를 `!isPaidLocked`로 제한했다. 홈·커뮤니티 리스트 상태를 수동 검증한다. | ## 최종 보고 형식 ```markdown 구현 결과: 홈·커뮤니티 아이템 통일과 상세 구매·댓글 잠금 일원화 - 변경: 홈 아이템 재사용, 목록 구매 제거, 더보기·상세·잠금 색상 통일, 상세 유료 미구매 댓글 차단 - 결정: `DEC-001~015`, `PLAN-DEC-001~008` - 검증: - 정적 contract check·`git diff --check` — 실제 결과 - `SodaLive-dev` Debug build — 실제 결과 - Preview·시뮬레이터 — 실제 결과 또는 외부 조건 - 남은 항목: `EXT-001` 반영 여부 또는 없음 - 문서: PRD·`plan-task.md`·Phase 3 review 리포트 ```