fix(main): 추천 커뮤니티 상세로 이동한다

This commit is contained in:
Yu Sung
2026-07-10 13:04:06 +09:00
parent 5213f0d98d
commit ce191539f2
8 changed files with 103 additions and 21 deletions

View File

@@ -14,6 +14,7 @@ struct CommunityPostCard: View {
let isLike: Bool
let likeCount: Int
let commentCount: Int
let onTapPost: () -> Void
let onTapLike: () -> Void
let onTapComment: () -> Void
let onTapPurchase: () -> Void
@@ -36,6 +37,7 @@ struct CommunityPostCard: View {
isLike: Bool,
likeCount: Int,
commentCount: Int,
onTapPost: @escaping () -> Void = {},
onTapLike: @escaping () -> Void = {},
onTapComment: @escaping () -> Void = {},
onTapPurchase: @escaping () -> Void = {}
@@ -52,6 +54,7 @@ struct CommunityPostCard: View {
self.isLike = isLike
self.likeCount = likeCount
self.commentCount = commentCount
self.onTapPost = onTapPost
self.onTapLike = onTapLike
self.onTapComment = onTapComment
self.onTapPurchase = onTapPurchase
@@ -76,6 +79,8 @@ struct CommunityPostCard: View {
.padding(SodaSpacing.s14)
.background(Color.gray900)
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
.contentShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
.onTapGesture(perform: onTapPost)
.onChange(of: isLike) { newValue in
localIsLike = newValue
}

View File

@@ -8,7 +8,7 @@ struct MainHomeView: View {
let onTapCreator: (Int) -> Void
let onTapContent: (Int) -> Void
let onTapCharacter: (Int) -> Void
let onTapCommunity: (Int) -> Void
let onTapCommunityPost: (Int) -> Void
let onTapBanner: (RecommendationBannerResponse) -> Void
let onTapFollowAll: (@escaping () -> Void) -> Void
let onSelectFollowingTab: () -> Bool
@@ -69,7 +69,7 @@ struct MainHomeView: View {
onTapCreator: onTapCreator,
onTapContent: onTapContent,
onTapCharacter: onTapCharacter,
onTapCommunity: onTapCommunity,
onTapCommunityPost: onTapCommunityPost,
onTapBanner: onTapBanner,
onTapFollowAll: onTapFollowAll
)
@@ -117,7 +117,7 @@ struct MainHomeView_Previews: PreviewProvider {
onTapCreator: { _ in },
onTapContent: { _ in },
onTapCharacter: { _ in },
onTapCommunity: { _ in },
onTapCommunityPost: { _ in },
onTapBanner: { _ in },
onTapFollowAll: { action in action() },
onSelectFollowingTab: { true },

View File

@@ -4,18 +4,18 @@ struct MainHomeActiveCreatorSection: View {
let items: [HomeActiveCreatorItem]
let onTapLive: (Int) -> Void
let onTapContent: (Int) -> Void
let onTapCommunity: (Int) -> Void
let onTapCommunityPost: (Int) -> Void
init(
items: [HomeActiveCreatorItem],
onTapLive: @escaping (Int) -> Void = { _ in },
onTapContent: @escaping (Int) -> Void = { _ in },
onTapCommunity: @escaping (Int) -> Void = { _ in }
onTapCommunityPost: @escaping (Int) -> Void = { _ in }
) {
self.items = items
self.onTapLive = onTapLive
self.onTapContent = onTapContent
self.onTapCommunity = onTapCommunity
self.onTapCommunityPost = onTapCommunityPost
}
var body: some View {
@@ -46,7 +46,7 @@ struct MainHomeActiveCreatorSection: View {
case .audio:
onTapContent(targetId)
case .community:
onTapCommunity(targetId)
onTapCommunityPost(targetId)
case .unknown:
break
}

View File

@@ -2,18 +2,18 @@ import SwiftUI
struct MainHomePopularCommunitySection: View {
let items: [HomePopularCommunityPostItem]
let onTapCommunity: (Int) -> Void
let onTapCommunityPost: (Int) -> Void
let onTapLike: (Int) -> Void
let onTapPurchase: (HomePopularCommunityPostItem) -> Void
init(
items: [HomePopularCommunityPostItem],
onTapCommunity: @escaping (Int) -> Void = { _ in },
onTapCommunityPost: @escaping (Int) -> Void = { _ in },
onTapLike: @escaping (Int) -> Void = { _ in },
onTapPurchase: @escaping (HomePopularCommunityPostItem) -> Void = { _ in }
) {
self.items = items
self.onTapCommunity = onTapCommunity
self.onTapCommunityPost = onTapCommunityPost
self.onTapLike = onTapLike
self.onTapPurchase = onTapPurchase
}
@@ -38,11 +38,14 @@ struct MainHomePopularCommunitySection: View {
isLike: item.isLiked,
likeCount: item.likeCount,
commentCount: item.commentCount,
onTapPost: {
onTapCommunityPost(item.postId)
},
onTapLike: {
onTapLike(item.postId)
},
onTapComment: {
onTapCommunity(item.creatorId)
onTapCommunityPost(item.postId)
},
onTapPurchase: {
onTapPurchase(item)

View File

@@ -5,7 +5,7 @@ struct MainHomeRecommendationView: View {
let onTapCreator: (Int) -> Void
let onTapContent: (Int) -> Void
let onTapCharacter: (Int) -> Void
let onTapCommunity: (Int) -> Void
let onTapCommunityPost: (Int) -> Void
let onTapBanner: (RecommendationBannerResponse) -> Void
let onTapFollowAll: (@escaping () -> Void) -> Void
@@ -37,7 +37,7 @@ struct MainHomeRecommendationView: View {
items: viewModel.recommendations?.recentlyActiveCreators ?? [],
onTapLive: onTapLive,
onTapContent: onTapContent,
onTapCommunity: onTapCommunity
onTapCommunityPost: onTapCommunityPost
)
MainHomeRecentDebutCreatorSection(
@@ -79,7 +79,7 @@ struct MainHomeRecommendationView: View {
MainHomePopularCommunitySection(
items: viewModel.recommendations?.popularCommunityPosts ?? [],
onTapCommunity: onTapCommunity,
onTapCommunityPost: onTapCommunityPost,
onTapLike: { postId in
viewModel.likeCommunityPost(postId: postId)
},
@@ -132,7 +132,7 @@ struct MainHomeRecommendationView_Previews: PreviewProvider {
onTapCreator: { _ in },
onTapContent: { _ in },
onTapCharacter: { _ in },
onTapCommunity: { _ in },
onTapCommunityPost: { _ in },
onTapBanner: { _ in },
onTapFollowAll: { action in action() }
)

View File

@@ -161,7 +161,7 @@ struct MainView: View {
onTapCreator: handleRecommendationCreatorTap,
onTapContent: handleRecommendationContentTap,
onTapCharacter: handleRecommendationCreatorTap,
onTapCommunity: handleRecommendationCommunityTap,
onTapCommunityPost: handleRecommendationCommunityPostTap,
onTapBanner: handleRecommendationBannerTap,
onTapFollowAll: handleRecommendationFollowAllTap,
onSelectFollowingTab: handleFollowingTabSelection,
@@ -494,10 +494,10 @@ struct MainView: View {
}
}
private func handleRecommendationCommunityTap(creatorId: Int) {
guard creatorId > 0 else { return }
private func handleRecommendationCommunityPostTap(postId: Int) {
guard postId > 0 else { return }
performRecommendationDetailAction {
appState.setAppStep(step: .creatorCommunityAll(creatorId: creatorId))
appState.setAppStep(step: .creatorChannelCommunityPostDetail(postId: postId))
}
}
@@ -527,7 +527,9 @@ struct MainView: View {
case .audio:
handleRecommendationContentTap(contentId: targetId)
case .community:
handleRecommendationCommunityTap(creatorId: targetId)
performRecommendationDetailAction {
appState.setAppStep(step: .creatorCommunityAll(creatorId: targetId))
}
case .unknown:
return
}

View File

@@ -477,8 +477,76 @@
- 실행 명령: `rg "### Phase [0-9]+:|- \\[[ x]\\] \\*\\*Task [0-9]+\\.[0-9]+:" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md`
- 기대 결과: phase heading과 `Task N.N` 형식 체크박스가 검색된다.
### Phase 20: 추천 커뮤니티 게시물 상세 진입 보정
- [x] **Task 20.1: PRD/계획 문서에 커뮤니티 게시물 상세 이동 요구사항 추가**
- 대상 파일:
- 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md`
- 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md`
- 작업 내용:
- 최근 활동 크리에이터의 `activityType == COMMUNITY` 탭 액션을 커뮤니티 게시물 상세 이동으로 명시한다.
- 인기 커뮤니티 섹션의 게시물 탭 액션을 커뮤니티 게시물 상세 이동으로 명시한다.
- `targetId`는 최근 활동 `COMMUNITY`일 때 커뮤니티 게시물 `postId`로 취급한다.
- 검증 기준:
- 실행 명령: `rg "activityType == COMMUNITY|커뮤니티 게시물 상세|targetId.*postId|인기 커뮤니티" docs/20260602_메인_홈_추천_UI_API_연동`
- 기대 결과: PRD와 계획 문서에서 이번 상세 이동 요구사항이 검색된다.
- [x] **Task 20.2: 추천 탭 커뮤니티 게시물 상세 라우팅 연결**
- 대상 파일:
- 수정: `SodaLive/Sources/V2/Main/MainView.swift`
- 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
- 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift`
- 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift`
- 수정: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomePopularCommunitySection.swift`
- 수정: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift`
- 작업 내용:
- 추천 탭 커뮤니티 callback을 `postId` 기준으로 명확히 연결한다.
- 최근 활동 크리에이터의 `COMMUNITY` 타입은 `targetId``postId`로 전달한다.
- 인기 커뮤니티 카드 일반 탭과 댓글 아이콘 탭은 `postId`로 커뮤니티 게시물 상세에 진입한다.
- 좋아요, 구매, 오디오 재생 액션은 기존 동작을 유지한다.
- 팔로잉 탭 스케줄의 기존 `.community` 라우팅은 이번 추천 탭 범위 밖이므로 변경하지 않는다.
- 검증 기준:
- 실행 명령: `rg "onTapCommunityPost|handleRecommendationCommunityPostTap|creatorChannelCommunityPostDetail|onTapPost|onTapComment|handleFollowingScheduleTap" SodaLive/Sources/V2/Main SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift`
- 기대 결과: 추천 탭 커뮤니티 액션은 `postId` 기반 상세 이동을 사용하고, 팔로잉 스케줄 community 라우팅은 기존 분기를 유지한다.
- [x] **Task 20.3: 정적 검증과 빌드 검증**
- 대상 파일:
- 확인: `SodaLive/Sources/V2/Main/**`
- 확인: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift`
- 확인: `SodaLive.xcodeproj/project.pbxproj`
- 작업 내용:
- 변경 파일의 공백 오류와 프로젝트 파일 유효성을 확인한다.
- 가능한 빌드를 실행한다.
- 실행 결과를 검증 기록에 누적한다.
- 검증 기준:
- 실행 명령: `git diff --check`
- 기대 결과: 출력 없이 성공한다.
- 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj`
- 기대 결과: `OK`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 기대 결과: `BUILD SUCCEEDED`
## 검증 기록
### 2026-07-10 Phase 20 추천 커뮤니티 게시물 상세 진입 보정 완료
- 목적: 메인 홈 추천 탭의 최근 활동 `COMMUNITY`와 인기 커뮤니티 게시물 탭 액션을 커뮤니티 게시물 상세 페이지로 연결
- 수행 내용:
- PRD에 최근 활동 `activityType == COMMUNITY``targetId`를 커뮤니티 게시물 `postId`로 취급한다는 기준을 추가
- PRD에 인기 커뮤니티 게시물 일반 탭/댓글 아이콘 탭이 커뮤니티 게시물 상세 페이지로 이동한다는 기준을 추가
- `MainHomeView`, `MainHomeRecommendationView`, `MainHomeActiveCreatorSection`, `MainHomePopularCommunitySection`의 추천 커뮤니티 callback 이름을 `onTapCommunityPost`로 정리
- `MainView.handleRecommendationCommunityPostTap(postId:)`를 추가해 추천 탭 커뮤니티 액션을 `AppStep.creatorChannelCommunityPostDetail(postId:onCommunityRefresh:)`로 연결
- `CommunityPostCard`에 기본 no-op `onTapPost`를 추가하고, 인기 커뮤니티 섹션에서 일반 탭과 댓글 아이콘 탭 모두 `postId` 기반 상세 이동으로 연결
- 팔로잉 스케줄의 기존 `.community` 라우팅은 `creatorCommunityAll(creatorId:)`로 유지하고 로그인 guard 흐름도 유지
- 검증:
- `rg "activityType == COMMUNITY|커뮤니티 게시물 상세|targetId.*postId|인기 커뮤니티" docs/20260602_메인_홈_추천_UI_API_연동` 실행, PRD/계획 문서에 요구사항 반영 확인
- `rg -n "onTapCommunityPost|handleRecommendationCommunityPostTap|creatorChannelCommunityPostDetail|onTapPost|onTapComment|handleFollowingScheduleTap|creatorCommunityAll" SodaLive/Sources/V2/Main SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` 실행, 추천 탭 `postId` 상세 이동과 팔로잉 스케줄 기존 분기 확인
- `rg -n "onTapCommunity:|handleRecommendationCommunityTap" SodaLive/Sources/V2/Main SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` 실행, 기존 모호한 추천 커뮤니티 callback 이름 제거 확인
- `git diff --check` 실행, 출력 없이 성공 확인
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` 실행, `OK` 확인
- 샌드박스 내 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 실행, CoreSimulator/로그 접근 제한으로 실패 확인
- 승인 후 동일 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 재실행, `BUILD SUCCEEDED` 확인
### 2026-06-30 Phase 17~19 최종 조립/검증/문서 갱신 완료
- 목적: V2 Main Home 추천 탭 섹션 조립 순서와 빈 섹션 숨김 조건을 최종 확인하고, 프로젝트 포함/정적 점검/빌드 검증 결과를 계획 문서에 반영

View File

@@ -179,6 +179,8 @@ enum class RecommendedActivityType(val code: String) {
- 민감 콘텐츠 보기 설정이 꺼져 있으면 `AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)`로 안내 문구를 예약하고 `AppState.shared.setAppStep(step: .contentViewSettings)`로 이동한다.
- 위 조건을 통과한 경우에만 `liveDetail`, `creatorDetail`, `contentDetail`, `characterDetail` 등 실제 상세 이동을 실행한다.
- 추천 탭 현재 라이브 아이템은 `roomId`가 항상 존재하는 값으로 취급하고, 아이템 탭 시 상세 화면을 거치지 않고 기존 `LiveViewModel.enterLiveRoom(roomId:)` 흐름을 호출해 방 상세 조회, 입장 전 결제 확인, 비밀방 비밀번호 dialog, `LiveRoomViewV2` 입장을 재사용한다.
- 최근 활동 크리에이터에서 `activityType == COMMUNITY`인 아이템은 `targetId`를 커뮤니티 게시물 `postId`로 취급하고, 탭 시 커뮤니티 게시물 상세 페이지로 이동한다.
- 인기 커뮤니티 섹션의 커뮤니티 게시물은 `postId` 기준으로 커뮤니티 게시물 상세 페이지로 이동한다. 좋아요, 구매, 오디오 재생 액션은 기존 개별 액션을 유지한다.
라이브 항목 주의사항:
- 기존 홈의 라이브 guard는 `roomId``isAdult`를 함께 받아 성인 여부를 판단한다.
@@ -204,6 +206,7 @@ enum class RecommendedActivityType(val code: String) {
- `구매완료` 버튼은 표시하지 않는다.
- 작성자, 본문, 생성 시간, 좋아요 수, 댓글 수를 표시한다.
- 생성 시간은 UTC 값을 디바이스 Timezone 기준 상대 날짜로 표시한다.
- 게시물 카드의 일반 탭 액션과 댓글 아이콘 탭 액션은 커뮤니티 게시물 상세 페이지로 이동한다.
### 7.5 사업자 정보 더보기/접기
@@ -298,6 +301,7 @@ MainHomeView
- `FeedCommunityView``구매완료` 버튼이 표시되지 않는다.
- 사업자 정보는 마지막 섹션에서 화면 width를 채우고, 기본 3줄 말줄임표, 더보기, 전체 표시, 접기 전환이 동작한다.
- 상세 진입 탭 액션은 기존 홈과 같은 로그인, 한국 사용자 본인인증, 민감 콘텐츠 보기 설정 guard를 통과한 뒤에만 실행된다.
- 최근 활동 크리에이터의 `COMMUNITY` 활동과 인기 커뮤니티 섹션의 게시물은 커뮤니티 게시물 상세 페이지로 이동한다.
- `MainHomeView`는 추천/랭킹/팔로잉 탭 shell 역할만 하고, 추천 탭 섹션 조립은 `MainHomeRecommendationView`에 분리되어 있다.
- 반복 UI는 기존 widget 또는 공용 컴포넌트를 재사용하고 불필요하게 중복 구현하지 않는다.
- 빌드가 성공하고, 가능하면 ViewModel 디코딩/상태 전환 검증을 수행한다.
@@ -307,7 +311,7 @@ MainHomeView
- 각 섹션별 최대 표시 개수와 가로/세로 스크롤 정책이 Figma 기준 그대로인지, 서버 응답 전체를 모두 표시해야 하는지 확인이 필요하다.
- `HomeLiveItem`에 민감/성인 라이브 여부를 판단할 `isAdult` 또는 동등한 필드가 추가되는지, 아니면 서버에서 추천 라이브를 사전 필터링하는지 확인이 필요하다.
- 커뮤니티 `audioUrl`을 추천 탭 카드에서 표시/재생해야 하는지 확인이 필요하다.
- 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다.
- 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다. 단, 최근 활동 `COMMUNITY`와 인기 커뮤니티 게시물은 커뮤니티 게시물 상세 페이지로 확정한다.
## 12. Verification Plan
- 문서 검증: API URL, 응답 모델, 제외 섹션, FeedCommunityView, 사업자 정보 요구가 PRD와 계획 문서에 반영되었는지 확인한다.