From 00c40161c9e50d58bf681acc518b4dda98b8375e Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Mon, 10 Aug 2026 15:42:46 +0900 Subject: [PATCH] =?UTF-8?q?fix(creator):=20=EA=B3=B5=EC=A7=80=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=9D=B4=EB=8F=99=EC=9D=84=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatorChannelNoticeSection.swift | 69 +++++++---------- .../Home/CreatorChannelHomeView.swift | 5 +- .../plan-task.md | 74 +++++++++++++++++-- .../prd.md | 16 +++- .../reviews/phase3-community-item-unification.md | 42 ++++++++--- 5 files changed, 145 insertions(+), 61 deletions(-) diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift index 6d95da47..c5adab4e 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelNoticeSection.swift @@ -2,20 +2,14 @@ import SwiftUI struct CreatorChannelNoticeSection: View { let notices: [CreatorChannelCommunityPostResponse] - let onTapNotice: (Int) -> Void - let onTapComment: (Int) -> Void - let onTapPurchase: (CreatorChannelCommunityPostResponse) -> Void + let onTapDetail: (Int) -> Void init( notices: [CreatorChannelCommunityPostResponse], - onTapNotice: @escaping (Int) -> Void = { _ in }, - onTapComment: @escaping (Int) -> Void = { _ in }, - onTapPurchase: @escaping (CreatorChannelCommunityPostResponse) -> Void = { _ in } + onTapDetail: @escaping (Int) -> Void = { _ in } ) { self.notices = notices - self.onTapNotice = onTapNotice - self.onTapComment = onTapComment - self.onTapPurchase = onTapPurchase + self.onTapDetail = onTapDetail } var body: some View { @@ -28,14 +22,8 @@ struct CreatorChannelNoticeSection: View { ForEach(notices) { notice in CreatorChannelNoticeCard( notice: notice, - onTapNotice: { - onTapNotice(notice.postId) - }, - onTapComment: { - onTapComment(notice.postId) - }, - onTapPurchase: { - onTapPurchase(notice) + onTapDetail: { + onTapDetail(notice.postId) } ) .frame(width: 374) @@ -51,9 +39,7 @@ struct CreatorChannelNoticeSection: View { private struct CreatorChannelNoticeCard: View { let notice: CreatorChannelCommunityPostResponse - let onTapNotice: () -> Void - let onTapComment: () -> Void - let onTapPurchase: () -> Void + let onTapDetail: () -> Void @StateObject private var playManager = CreatorCommunityMediaPlayerManager.shared @StateObject private var contentPlayManager = ContentPlayManager.shared @@ -69,7 +55,7 @@ private struct CreatorChannelNoticeCard: View { .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) .contentShape(Rectangle()) - .onTapGesture(perform: onTapNotice) + .onTapGesture(perform: onTapDetail) } private var noticeLabel: some View { @@ -164,40 +150,37 @@ private struct CreatorChannelNoticeCard: View { } private var paidLockOverlay: some View { - Button(action: onTapPurchase) { - ZStack { - Color.gray700.opacity(0.35) + ZStack { + Color.gray700.opacity(0.35) - VStack(spacing: SodaSpacing.s4) { - Image("ic_new_community_lock") + VStack(spacing: SodaSpacing.s4) { + Image("ic_new_community_lock") + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .frame(width: 24, height: 24) + + HStack(spacing: SodaSpacing.s6) { + Image("ic_bar_cash") .resizable() - .renderingMode(.template) - .foregroundColor(.white) - .frame(width: 24, height: 24) + .frame(width: 20, height: 20) - HStack(spacing: SodaSpacing.s6) { - Image("ic_bar_cash") - .resizable() - .frame(width: 20, height: 20) - - Text("\(notice.price)") - .appFont(.body2) - .foregroundColor(.black) - } - .padding(SodaSpacing.s8) - .background(Color.white) - .clipShape(Capsule()) + Text("\(notice.price)") + .appFont(.body2) + .foregroundColor(.black) } + .padding(SodaSpacing.s8) + .background(Color.white) + .clipShape(Capsule()) } } - .buttonStyle(.plain) .frame(width: 92, height: 92) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } private var reactionBar: some View { HStack(spacing: 15) { - Button(action: onTapComment) { + Button(action: onTapDetail) { HStack(spacing: SodaSpacing.s4) { Image("ic_feed_community_reply") .resizable() diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index c9297578..441dc689 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -57,7 +57,10 @@ struct CreatorChannelHomeView: View { onTapDonate: onTapDonate ) - CreatorChannelNoticeSection(notices: response.notices) + CreatorChannelNoticeSection( + notices: response.notices, + onTapDetail: onTapCommunityDetail + ) CreatorChannelScheduleSection( schedules: response.schedules, diff --git a/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/plan-task.md b/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/plan-task.md index d299eef0..34672f9d 100644 --- a/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/plan-task.md +++ b/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/plan-task.md @@ -570,6 +570,50 @@ Expected: 첫 명령 match 0건·exit code 1, 둘째 명령 match 2건, whitespa - [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 구현은 변경하지 않는다. + ### Phase 2 Gate **Goal 실행 `P2-GATE`:** 홈 재사용·상세·더보기·목록 동기화 구조를 판정한다. @@ -579,10 +623,12 @@ 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 xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build ``` -Expected: whitespace 오류 0건, 필수 연결 match, 홈 `CommunityPostCard` match 0건·exit code 1, `** BUILD SUCCEEDED **`. +Expected: whitespace 오류 0건, 필수 연결 match, 홈 `CommunityPostCard`·공지 기존 callback match 0건·exit code 1, `** BUILD SUCCEEDED **`. ## Phase 3: 리뷰·통합 검증·문서 종결 @@ -620,6 +666,8 @@ Expected: whitespace 오류 0건, 필수 연결 match, 홈 `CommunityPostCard` m 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 ``` @@ -662,12 +710,13 @@ Expected: exit code 0, `** BUILD SUCCEEDED **`. | 5 | `P2-T1` | `P1-GATE` | 아니요 | 홈·탭 모델 매핑 재확인 | | 6 | `P2-T2` | `P2-T1` | 아니요 | mutation 성공 callback 호출처 재확인 | | 7 | `P2-R1` | 사용자 회귀 제보·PRD 반영 | 아니요 | reaction bar 삽입 조건 재확인 | -| 8 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task 수정 | -| 9 | `P3-T1` | `P2-GATE` | 아니요 | 리뷰 수정 Task 추가 | -| 10 | `P3-GATE` | `P3-T1`·`EXT-001` | 아니요 | 백엔드 반영 후 통합 검증 재개 | +| 8 | `P2-R2` | 사용자 후속 요청·PRD 반영 | 아니요 | 공지 상세 callback 경계 재확인 | +| 9 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task 수정 | +| 10 | `P3-T1` | `P2-GATE` | 아니요 | 리뷰 수정 Task 추가 | +| 11 | `P3-GATE` | `P3-T1`·`EXT-001` | 아니요 | 백엔드 반영 후 통합 검증 재개 | ```text -P1-T1 → P1-T2 → P1-T3 → P1-GATE → P2-T1 → P2-T2 → P2-R1 → P2-GATE → P3-T1 → P3-GATE +P1-T1 → P1-T2 → P1-T3 → P1-GATE → P2-T1 → P2-T2 → P2-R1 → P2-R2 → P2-GATE → P3-T1 → P3-GATE ``` ## 변경 금지 항목 @@ -749,6 +798,19 @@ P1-T1 → P1-T2 → P1-T3 → P1-GATE → P2-T1 → P2-T2 → P2-R1 → P2-GATE - 남은 항목: 기존 `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`를 재개한다. + ## Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | @@ -759,6 +821,7 @@ P1-T1 → P1-T2 → P1-T3 → P1-GATE → P2-T1 → P2-T2 → P2-R1 → P2-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` | ## 발견된 문제 @@ -767,6 +830,7 @@ P1-T1 → P1-T2 → P1-T3 → P1-GATE → P2-T1 → P2-T2 → P2-R1 → P2-GATE | `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 통과. | ## 최종 보고 형식 diff --git a/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md b/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md index dff4d90e..a8713a29 100644 --- a/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md +++ b/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md @@ -4,7 +4,7 @@ | 항목 | 내용 | |---|---| -| 문서 상태 | reaction bar 회귀 수정·Debug build 완료·`EXT-001`/Simulator 통합 검증 대기 | +| 문서 상태 | 공지 상세 구매 일원화·Debug build 완료·`EXT-001`/Simulator 통합 검증 대기 | | 작성일 | 2026-08-10 | | 최종 수정일 | 2026-08-10 | | 대상 제품 | SodaLive iOS 크리에이터 채널 홈·커뮤니티 | @@ -15,7 +15,7 @@ ## 1. Overview -크리에이터 채널 홈 탭의 커뮤니티 요약 아이템을 커뮤니티 탭 리스트와 동일한 `CreatorChannelCommunityListItem`으로 표시한다. 홈과 커뮤니티 탭의 아이템 터치는 게시글 상세로 이동하고, 유료 게시글 구매는 상세 화면에서만 시작한다. 유료 미구매 이미지 영역은 상세 화면의 `Color.gray800`과 동일하게 통일하고, 하트·댓글을 모두 표시하지 않는 상태에서는 reaction bar의 빈 높이도 제거한다. +크리에이터 채널 홈 탭의 커뮤니티 요약 아이템을 커뮤니티 탭 리스트와 동일한 `CreatorChannelCommunityListItem`으로 표시한다. 홈의 커뮤니티·공지와 커뮤니티 탭의 아이템 터치는 게시글 상세로 이동하고, 유료 게시글 구매는 상세 화면에서만 시작한다. 유료 미구매 이미지 영역은 상세 화면의 `Color.gray800`과 동일하게 통일하고, 하트·댓글을 모두 표시하지 않는 상태에서는 reaction bar의 빈 높이도 제거한다. ## 2. Problem Statement @@ -24,6 +24,7 @@ - 커뮤니티 탭의 유료 미구매 영역은 `Color.gray400`, 상세 화면은 `Color.gray800`을 사용해 색상이 다르다. - 현재 `CreatorChannelCommunityListItem`의 유료 미구매 영역은 목록에서 구매 다이얼로그를 여는 별도 `Button`이므로, 전체 아이템을 상세 진입으로 통일하려는 요구와 충돌한다. - 홈·커뮤니티 리스트와 상세는 하트·댓글을 모두 숨기는 유료 잠금 상태에서도 높이 24pt의 빈 reaction bar를 삽입해 불필요한 세로 공간이 남는다. +- 홈 공지 카드는 상세·댓글·구매 callback이 기본 no-op이고, 유료 잠금 영역에 목록 구매용 `Button`이 남아 있어 상세 전용 구매 원칙에서 제외되어 있다. ## 3. Goals @@ -32,6 +33,7 @@ - 홈 탭과 커뮤니티 탭의 리스트형 커뮤니티 아이템 외형을 하나의 컴포넌트로 통일한다. - 홈 탭 커뮤니티 아이템 우측 상단에 더보기를 제공한다. - 커뮤니티 아이템 터치를 상세 진입으로 통일하고 구매는 상세 화면에서 처리한다. +- 홈 공지 게시글도 본체·댓글·유료 잠금 영역을 상세 진입으로 통일한다. - 유료 미구매 이미지 영역을 `Color.gray800`으로 통일한다. - 하트·댓글을 모두 표시하지 않는 상태에서는 reaction bar가 레이아웃 공간을 차지하지 않게 한다. @@ -57,7 +59,7 @@ ## 6. 핵심 사용자 흐름 -1. 사용자가 크리에이터 채널 홈의 커뮤니티 요약 아이템을 확인한다. +1. 사용자가 크리에이터 채널 홈의 커뮤니티 요약 또는 공지 아이템을 확인한다. 2. 아이템 본체를 터치하면 해당 `postId`의 커뮤니티 게시글 상세로 이동한다. 3. 유료 미구매 게시글이면 상세 화면에서 기존 구매 다이얼로그와 API를 사용한다. 4. 사용자가 우측 상단 더보기를 터치하면 확정된 권한 규칙에 맞는 메뉴가 표시된다. @@ -75,6 +77,7 @@ | `CCCI-007` | 확정 | 홈과 커뮤니티 탭의 리스트형·썸네일형 모두 목록 구매를 제거하고 유료 미구매 영역 터치를 상세 이동으로 처리한다. | 모든 목록 표시 모드에서 구매 다이얼로그가 열리지 않고 상세에서만 구매한다. | `P1-T2`, `P1-T3` | | `CCCI-008` | 외부 의존 | 백엔드 홈 API의 `communities`·`notices` 아이템에 `isCommentAvailable: Bool`·`isPinned: Bool`을 커뮤니티 탭과 동일하게 제공한다. | iOS가 두 필드를 임의 기본값 없이 디코딩해 고정·댓글 상태를 표시한다. | `P1-T1`, `EXT-001` | | `CCCI-009` | 확정 | 홈·커뮤니티 리스트와 커뮤니티 상세에서 하트·댓글을 모두 표시하지 않을 때 reaction bar를 삽입하지 않는다. | 유료 미구매 잠금 상태에서 하트·댓글과 고정 24pt 높이가 모두 사라지고, 하나라도 표시되면 기존 reaction bar가 유지된다. | `P2-R1` | +| `CCCI-010` | 확정 | 홈 공지 게시글도 목록 구매를 제거하고 본체·댓글·유료 잠금 영역을 상세 이동으로 처리한다. | 공지 카드에서 구매 다이얼로그가 직접 열리지 않고 세 영역 모두 선택한 `postId`의 상세로 이동한다. | `P2-R2` | ## 8. UI/UX Expectations @@ -104,6 +107,7 @@ - [x] 홈·리스트형·썸네일형·상세의 유료 미구매 영역이 모두 `Color.gray800`을 사용한다. - [ ] 더보기·좋아요 터치가 상세 이동을 중복 발생시키지 않는다. - [x] 홈·커뮤니티 리스트와 상세의 유료 미구매 상태에서 하트·댓글용 빈 공간이 남지 않는다. +- [ ] 홈 공지의 본체·댓글·유료 잠금 영역이 상세로 이동하고 공지 카드에서 구매 다이얼로그가 직접 열리지 않는다. - [x] `SodaLive-dev` 스킴 Debug 빌드가 성공한다. - [ ] 시뮬레이터에서 홈·커뮤니티 탭·상세의 아이템 터치, 더보기, 유료 잠금 색상을 확인한다. @@ -130,6 +134,7 @@ | `CCCI-004`, `CCCI-005`, `CCCI-007` | 기존 목록·상세·구매 API 재사용 | `P1-T2`, `P1-T3` | 목록 구매 제거·상세 구매 연결·`Color.gray800` 참조 검증·Debug build | 목록/상세 잠금 영역·상세 진입·구매 확인 | | `CCCI-008` | 홈 API 필드 확장 | `P1-T1`, `P3-GATE`, `EXT-001` | decoding contract·Debug build | 백엔드 반영 후 홈 응답 확인 | | `CCCI-009` | 기존 목록·상세 모델 재사용 | `P2-R1` | reaction bar 조건부 삽입 contract check·Debug build | 홈·커뮤니티 리스트·상세 유료 잠금 여백 확인 | +| `CCCI-010` | 기존 홈 상세 route·상세 구매 API 재사용 | `P2-R2` | 공지 구매 callback 제거·상세 callback 연결 contract check·Debug build | 공지 본체·댓글·유료 잠금 상세 진입 확인 | ## 14. Decision Log @@ -148,6 +153,7 @@ | 2026-08-10 | `DEC-011` | 확정 | 승인된 컴포넌트·데이터 흐름과 최종 동작·오류·검증 설계를 구현 기준으로 사용한다. | 사용자 최종 설계 승인 | `CCCI-001~008`, `EXT-001` | | 2026-08-10 | `DEC-012` | 확정 | 목록에 있던 기존 구매 다이얼로그·API 호출 소유권을 상세 화면으로 옮긴다. | 구현 전 기준선에서 상세 잠금 버튼이 구매가 아닌 `showPurchaseError()`만 호출함을 확인·사용자 직접 요구 충족 | `CCCI-004`, `CCCI-007` | | 2026-08-10 | `DEC-013` | 확정 | 하트·댓글이 모두 숨겨지는 유료 잠금 상태에서는 고정 높이를 보정하지 않고 reaction bar 전체를 조건부로 삽입하지 않는다. | 목록·상세의 공통 원인은 빈 `HStack`의 `.frame(height: 24)` 유지이며, 상위 조건부 삽입이 가장 작은 근본 수정이다. | `CCCI-009`, `P2-R1` | +| 2026-08-10 | `DEC-014` | 확정 | 홈 공지도 별도 구매 callback을 제거하고 기존 `onTapCommunityDetail`·`showCommunityPostDetail(_:)` 흐름을 재사용한다. | 공지도 커뮤니티 게시글이며 구매는 상세에서만 가능해야 한다는 사용자 요청 | `CCCI-010`, `P2-R2` | ## 15. 구현 설계 @@ -167,6 +173,7 @@ - 상세 진입 helper는 표시 모델 전체 대신 `postId` 만 받아 홈·리스트형·썸네일형이 같은 route를 사용한다. - 더보기·좋아요 `Button`은 각자의 action만 실행하고 부모 아이템의 상세 진입을 중복 실행하지 않아야 한다. - `CreatorChannelCommunityListItem`과 `CreatorChannelCommunityPostDetailContentView`는 `isPaidLocked`가 `true`이면 `reactionBar` 자체를 View 계층에 삽입하지 않는다. 댓글만 비활성이고 좋아요가 표시되는 상태는 기존 reaction bar를 유지한다. +- `CreatorChannelNoticeSection`은 본체·댓글·유료 잠금 영역에 하나의 상세 callback을 사용하고, `CreatorChannelHomeView`의 기존 `onTapCommunityDetail`을 전달한다. 공지 카드의 외형은 변경하지 않는다. ### 15.3 더보기와 변경 후 갱신 @@ -189,6 +196,7 @@ - 대체 검증으로 홈 섹션·리스트형·썸네일형 Preview에서 유료 미구매·고정·댓글 비활성 상태를 구성한다. - 정적 검증은 `rg`로 목록 `onTapPurchase`·목록 구매 dialog 호출처 제거, 상세 구매 dialog·API 연결, `Color.gray800`, 홈·목록·썸네일 상세 route 연결을 확인하고 `git diff --check`를 실행한다. - reaction bar 회귀 검증은 두 대상 View의 body에 무조건 삽입된 `reactionBar`가 없고 `if !isPaidLocked` 내부에만 삽입되는지 focused `rg`로 확인한다. +- 공지 회귀 검증은 `CreatorChannelNoticeSection`의 `onTapPurchase`·`onTapNotice`·`onTapComment`가 제거되고 단일 상세 callback이 `CreatorChannelHomeView.onTapCommunityDetail`에 연결되는지 focused `rg`로 확인한다. - 자동 검증은 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 컴파일·링크 성공을 확인한다. - `EXT-001` 반영 후 시뮬레이터에서 홈·리스트형·썸네일형의 상세 진입, 더보기 권한 메뉴, 목록 구매 dialog 미노출, 상세 구매, `Color.gray800`을 수동 확인한다. @@ -206,3 +214,5 @@ - 2026-08-10: 사용 가능한 Simulator가 모두 `Shutdown`이고 `EXT-001` 반영 여부를 확인할 수 없어 실제 터치·메뉴·구매 통합 검증은 대기한다. - 2026-08-10: 사용자 제보를 따라 목록·상세를 추적한 결과, 유료 잠금 상태에서도 빈 `reactionBar`의 `.frame(height: 24)`가 유지되는 공통 원인을 확인하고 `CCCI-009`, `DEC-013`, `P2-R1`로 회귀 수정 범위를 확정했다. - 2026-08-10: 두 body의 무조건 `reactionBar` 삽입을 `if !isPaidLocked` 조건부 삽입으로 변경했다. focused `rg`에서 무조건 삽입 0건·조건부 삽입 2건, `git diff --check` 오류 0건, 동일 iPhone 17 Pro 대상 `SodaLive-dev` Debug build exit code 0을 확인했다. +- 2026-08-10: `CreatorChannelNoticeSection`에 기본 no-op인 상세·댓글 callback과 별도 구매 callback·잠금 `Button`이 남고 `CreatorChannelHomeView`가 상세 callback을 전달하지 않는 `REV-P3-004`를 확인해 `CCCI-010`, `DEC-014`, `P2-R2`로 수정 범위를 확정했다. +- 2026-08-10: 공지 callback을 단일 `onTapDetail`로 통일하고 잠금 영역의 직접 구매 `Button`을 제거한 뒤 `CreatorChannelHomeView.onTapCommunityDetail`에 연결했다. RED 계약 검사는 직접 구매 callback과 미연결 생성부를 각각 검출했고, GREEN은 기존 callback 0건·상세 연결 match, `git diff --check` 오류 0건, `SodaLive-dev` Debug build exit code 0을 확인했다. diff --git a/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/reviews/phase3-community-item-unification.md b/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/reviews/phase3-community-item-unification.md index 147a4587..ac759477 100644 --- a/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/reviews/phase3-community-item-unification.md +++ b/docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/reviews/phase3-community-item-unification.md @@ -4,18 +4,18 @@ | 항목 | 내용 | |---|---| -| 리뷰 대상 | Phase 1~2 / `P1-T1~P2-T2` | +| 리뷰 대상 | Phase 1~2 / `P1-T1~P2-R2` | | 기준 commit 또는 working tree | `89c070875cbeea8395c1e90cb9f47ff80df5d90f` 이후 미커밋 변경 | | 리뷰 일자 | 2026-08-10 | | 리뷰어 | Codex 자가 리뷰—프로젝트 보수적 실행 정책에 따라 subagent 미사용 | | 기준 문서 | `docs/20260810_크리에이터_채널_커뮤니티_아이템_통일/prd.md`, `plan-task.md` | -| 리뷰 상태 | `REV-P3-003` 수정 완료·외부 통합 검증 대기 | +| 리뷰 상태 | `REV-P3-004` 수정 완료·외부 통합 검증 대기 | ## 2. 리뷰 목적과 범위 ### 목적 -- 홈·커뮤니티 탭의 아이템, 상세 진입, 더보기, 목록 구매 제거, 상세 구매 이동이 `CCCI-001~008`을 충족하는지 확인한다. +- 홈·커뮤니티 탭의 아이템과 공지, 상세 진입, 더보기, 목록 구매 제거, 상세 구매 이동이 `CCCI-001~010`을 충족하는지 확인한다. - 코드·문서·검증 기록이 실제 working tree와 일치하는지 확인한다. ### 포함 범위 @@ -50,8 +50,8 @@ ### 문서와 코드 -- 요구사항: `CCCI-001~009`, `DEC-001~013`, `EXT-001` -- 계획: `P1-T1~P2-T2`, `P2-R1`, `P1-GATE`, `P2-GATE` +- 요구사항: `CCCI-001~010`, `DEC-001~014`, `EXT-001` +- 계획: `P1-T1~P2-T2`, `P2-R1`, `P2-R2`, `P1-GATE`, `P2-GATE` - 홈 변환·아이템: `CreatorChannelCommunitySection.swift:3` - 목록 잠금·상세 gesture: `CreatorChannelCommunityListItem.swift`, `CreatorChannelCommunityGridItem.swift` - 상세 구매: `CreatorChannelCommunityPostDetailView.swift:19`, `CreatorChannelCommunityPostDetailViewModel.swift:186` @@ -75,19 +75,21 @@ Backend: EXT-001 반영 여부를 이 환경에서 확인하지 못함 | 목록 구매 심볼·잠금 배경 `rg` contract check | 성공 | 목록 구매 심볼과 잠금 `Color.gray400` 0건, 목록·상세 `Color.gray800` match | | 상세 구매 `rg` contract check | 성공 | `showPurchaseError` 0건, 상세 dialog·repository 호출 match | | reaction bar 조건부 삽입 `rg` contract check | 성공 | 무조건 삽입 0건, `if !isPaidLocked` 조건부 삽입 2건 | +| 공지 상세 전용 구매 `rg` RED/GREEN contract check | 성공 | RED 직접 구매 callback 7건·미연결 생성부 1건, GREEN 기존 callback 0건·상세 연결 match | | `git diff --check` | 성공 | whitespace 오류 0건 | | `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -destination 'platform=iOS Simulator,id=E9FC7721-AA96-440F-8349-2DC4B85F40F5' -derivedDataPath /tmp/sodalive-community-baseline CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES build` | 성공 | exit code 0 | | `xcrun simctl list devices` | 수동 검증 불가 | 모든 사용 가능한 Simulator가 `Shutdown`; 스킬 정책상 임의 부팅하지 않음 | ## 5. 발견 사항 요약 -사용자 회귀 제보로 확정한 reaction bar 빈 공간 1건은 수정·빌드를 완료했다. +사용자 회귀 제보로 확정한 reaction bar 빈 공간 1건은 수정·빌드를 완료했고, 홈 공지의 상세 전용 구매 원칙 누락 1건을 추가 확정했다. | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-P3-001` | High | 수정 완료 | 상세 잠금 버튼이 구매 대신 공통 오류만 표시하던 계획 누락 | `P1-T3` | 완료 | | `REV-P3-002` | High | 보류 | 홈 API 신규 필드와 런타임 동작의 통합 검증 필요 | `P3-GATE`, `EXT-001` | 외부 반영 후 재개 | | `REV-P3-003` | Medium | 수정 완료 | 하트·댓글이 숨겨져도 빈 reaction bar가 24pt 공간을 차지함 | `P2-R1` | 완료 | +| `REV-P3-004` | High | 수정 완료 | 홈 공지에 목록 구매 callback이 남고 상세 callback이 연결되지 않음 | `P2-R2` | 완료 | ## 6. 발견 사항 상세 @@ -118,10 +120,20 @@ Backend: EXT-001 반영 여부를 이 환경에서 확인하지 못함 `CreatorChannelCommunityListItem`과 `CreatorChannelCommunityPostDetailContentView`는 유료 잠금 상태에서 하트·댓글 자식만 숨기고, body에는 `reactionBar`를 항상 삽입했다. 그 결과 빈 `HStack`의 `.frame(height: 24)`와 부모 `VStack` spacing이 홈·커뮤니티 리스트·상세에 남았다. 코드 수정 전에 `P2-R1`을 추가하고, 두 body에서 기존 `isPaidLocked`로 reaction bar 전체를 조건부 제외했다. focused contract check·`git diff --check`·Debug build가 통과했다. +### REV-P3-004 — 홈 공지의 상세 전용 구매 원칙 누락 + +- **심각도:** High +- **상태:** 수정 완료 +- **관련 요구사항:** `CCCI-010` +- **소유 Task:** `P2-R2` + +`CreatorChannelNoticeSection`은 `onTapNotice`·`onTapComment`·`onTapPurchase`를 각각 기본 no-op으로 선언하고 잠금 영역에 별도 구매 `Button`을 뒀다. `CreatorChannelHomeView`는 notices만 전달해 상세 callback을 연결하지 않았다. 코드 수정 전에 `P2-R2`를 추가하고, 세 callback을 단일 `onTapDetail`로 통일해 기존 `onTapCommunityDetail` route에 연결했다. 잠금 구매 `Button` wrapper만 제거했으며 카드 외형과 상세 구매·refresh 구현은 변경하지 않았다. focused RED/GREEN contract check·`git diff --check`·Debug build가 통과했다. + ## 7. 확정 항목의 plan·goal 전환 - `REV-P3-001`: 코드 수정 전에 `P1-T3`로 전환했고 수정·빌드를 완료했다. - `REV-P3-003`: 코드 수정 전에 `P2-R1`로 전환했고 수정·검증을 완료했다. +- `REV-P3-004`: 코드 수정 전에 `P2-R2`로 전환했고 수정·검증을 완료했다. - `REV-P3-002`: 외부 조건이므로 `P3-GATE` 완료를 보류한다. ## 8. 리뷰 종료 판정 @@ -129,12 +141,12 @@ Backend: EXT-001 반영 여부를 이 환경에서 확인하지 못함 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | 사용자 회귀 제보와 수정 결과를 추가 반영 | -| 후보 항목 판정 완료 | 충족 | `REV-P3-001`·`REV-P3-003` 수정 완료, `REV-P3-002` 보류 | -| 확정 항목 plan 반영 | 충족 | `P1-T3`, `P2-R1`, `DEC-012~013` | +| 후보 항목 판정 완료 | 충족 | `REV-P3-001`·`REV-P3-003`·`REV-P3-004` 수정 완료, `REV-P3-002` 보류 | +| 확정 항목 plan 반영 | 충족 | `P1-T3`, `P2-R1`, `P2-R2`, `DEC-012~014` | | 보류 항목의 담당·재개 조건 기록 | 충족 | `EXT-001`·부팅된 Simulator | | 검증 명령과 결과 기록 | 충족 | 정적 계약·Debug build exit 0 | -**최종 결론:** iOS reaction bar 회귀 수정·빌드 완료, 외부 통합 검증 대기 +**최종 결론:** 홈 공지 상세 구매 일원화 수정·빌드 완료, 외부 통합 검증 대기 **남은 항목:** 백엔드 `EXT-001` 반영 후 부팅된 Simulator에서 본체 상세 이동, 권한별 더보기, 목록 dialog 미노출, 상세 구매, `Color.gray800`을 확인한다. @@ -159,3 +171,15 @@ Backend: EXT-001 반영 여부를 이 환경에서 확인하지 못함 - `git diff --check` — 성공 - `SodaLive-dev` 단일 Simulator Debug build — exit code 0 - 남은 항목: `REV-P3-002`와 Simulator 시각 검증 + +### 3차 수정 검증 — 2026-08-10 + +- 무엇을: `REV-P3-004`의 홈 공지 직접 구매·상세 미연결을 수정했다. +- 왜: 공지도 커뮤니티 게시글이므로 목록이 아닌 상세에서만 구매해야 한다. +- 어떻게: + - RED: 직접 구매 callback 7건, 홈 상세 callback 미연결 생성부 1건 확인 + - GREEN: 기존 callback 0건, 단일 `onTapDetail`·홈 `onTapCommunityDetail` 연결 확인 + - 기존 상세 구매 dialog/API와 구매 후 상세·홈·커뮤니티 refresh 연결 유지 확인 + - `git diff --check` — 성공 + - `SodaLive-dev` Debug build — exit code 0 +- 남은 항목: `REV-P3-002`와 Simulator 통합 검증