From aa735b7053ecb79c9abd269767b729327e28b80f Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 3 Jul 2026 15:58:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EC=84=B9=EC=85=98=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 --- SodaLive.xcodeproj/project.pbxproj | 8 ++ .../V2/Component/Card/CommunityPostCard.swift | 119 +++++++++++------- .../CreatorChannelCommunitySection.swift | 118 +++++++++++++++++ .../Home/CreatorChannelHomeView.swift | 5 + docs/20260701_크리에이터_채널_홈/plan-task.md | 2 +- 5 files changed, 203 insertions(+), 49 deletions(-) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 41664efe..853913c9 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -46,6 +46,9 @@ CC01011B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */; }; CC01021B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */; }; CC01031B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */; }; + CC01011C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */; }; + CC01021C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */; }; + CC01031C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */; }; CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; @@ -1778,6 +1781,7 @@ CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioContentListItem.swift; sourceTree = ""; }; CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioSection.swift; sourceTree = ""; }; CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSeriesSection.swift; sourceTree = ""; }; + CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelCommunitySection.swift; sourceTree = ""; }; 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = ""; }; 0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = ""; }; 189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = ""; }; @@ -2705,6 +2709,7 @@ CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */, CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */, CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */, + CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */, ); path = Components; sourceTree = ""; @@ -5647,6 +5652,7 @@ CC0101192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC01011A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, CC01011B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, + CC01011C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6452,6 +6458,7 @@ CC0102192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC01021A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, CC01021B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, + CC01021C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7273,6 +7280,7 @@ CC0103192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC01031A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, CC01031B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, + CC01031C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */, CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, diff --git a/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift b/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift index 09658ae5..cb44d107 100644 --- a/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift +++ b/SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift @@ -1,4 +1,5 @@ import SwiftUI +import Kingfisher struct CommunityPostCard: View { let postId: Int @@ -67,8 +68,11 @@ struct CommunityPostCard: View { imageContent } - reactionBar + if isAccessiblePost { + reactionBar + } } + .frame(maxWidth: .infinity, alignment: .leading) .padding(SodaSpacing.s14) .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) @@ -110,28 +114,26 @@ struct CommunityPostCard: View { .lineLimit(5) .truncationMode(.tail) .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) } private var imageContent: some View { - GeometryReader { proxy in - ZStack { - DownsampledKFImage( - url: URL(string: imageUrl ?? ""), - size: CGSize(width: proxy.size.width, height: proxy.size.height) - ) + ZStack { + KFImage(URL(string: imageUrl ?? "")) + .cancelOnDisappear(true) + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) .background(Color.gray800) - .frame(width: proxy.size.width, height: proxy.size.height) - .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) .blur(radius: isLocked ? 12 : 0) - if isLocked { - paidLockOverlay - } else if hasAudio { - audioPlayButton - } + if isLocked { + paidLockOverlay + } else if hasAudio { + audioPlayButton } } - .aspectRatio(346.0 / 236.0, contentMode: .fit) + .frame(maxWidth: .infinity) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } @@ -148,7 +150,7 @@ struct CommunityPostCard: View { .frame(width: 24, height: 24) HStack(spacing: SodaSpacing.s6) { - Image("ic_can") + Image("ic_bar_cash") .resizable() .frame(width: 20, height: 20) @@ -224,6 +226,10 @@ struct CommunityPostCard: View { price > 0 && !existOrdered } + private var isAccessiblePost: Bool { + price == 0 || existOrdered + } + private func toggleLike() { localIsLike.toggle() localLikeCount += localIsLike ? 1 : -1 @@ -236,39 +242,56 @@ struct CommunityPostCard_Previews: PreviewProvider { private static let previewImageUrl = "https://picsum.photos/500/500" static var previews: some View { - VStack(spacing: SodaSpacing.s8) { - CommunityPostCard( - postId: 1, - creatorNickname: "크리에이터 이름", - creatorProfileImageUrl: previewImageUrl, - content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", - imageUrl: nil, - audioUrl: nil, - price: 0, - existOrdered: false, - createdAt: "2분 전", - isLike: false, - likeCount: 5, - commentCount: 6 - ) + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: SodaSpacing.s8) { + CommunityPostCard( + postId: 1, + creatorNickname: "크리에이터 이름", + creatorProfileImageUrl: previewImageUrl, + content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", + imageUrl: nil, + audioUrl: nil, + price: 0, + existOrdered: false, + createdAt: "2분 전", + isLike: false, + likeCount: 5, + commentCount: 6 + ) - CommunityPostCard( - postId: 2, - creatorNickname: "크리에이터 이름", - creatorProfileImageUrl: previewImageUrl, - content: "이미지가 있는 커뮤니티 게시글입니다.", - imageUrl: previewImageUrl, - audioUrl: "https://example.com/audio.mp3", - price: 30, - existOrdered: false, - createdAt: "2분 전", - isLike: true, - likeCount: 5, - commentCount: 6 - ) + CommunityPostCard( + postId: 2, + creatorNickname: "크리에이터 이름", + creatorProfileImageUrl: previewImageUrl, + content: "이미지가 있는 커뮤니티 게시글입니다.", + imageUrl: previewImageUrl, + audioUrl: "https://example.com/audio.mp3", + price: 30, + existOrdered: false, + createdAt: "2분 전", + isLike: true, + likeCount: 5, + commentCount: 6 + ) + + CommunityPostCard( + postId: 3, + creatorNickname: "크리에이터 이름", + creatorProfileImageUrl: previewImageUrl, + content: "이미지가 있는 커뮤니티 게시글입니다.", + imageUrl: previewImageUrl, + audioUrl: "https://example.com/audio.mp3", + price: 0, + existOrdered: false, + createdAt: "2분 전", + isLike: true, + likeCount: 5, + commentCount: 6 + ) + } + .padding(SodaSpacing.s14) + .background(Color.black) + .previewLayout(.sizeThatFits) } - .padding(SodaSpacing.s14) - .background(Color.black) - .previewLayout(.sizeThatFits) } } diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift new file mode 100644 index 00000000..e7bb3208 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift @@ -0,0 +1,118 @@ +import SwiftUI + +struct CreatorChannelCommunitySection: View { + let communities: [CreatorChannelCommunityPostResponse] + let onSelectTab: (CreatorChannelTab) -> Void + let onTapLike: (Int) -> Void + let onTapComment: (Int) -> Void + let onTapPurchase: (CreatorChannelCommunityPostResponse) -> Void + + private let homeVisibleCount = 3 + + init( + communities: [CreatorChannelCommunityPostResponse], + onSelectTab: @escaping (CreatorChannelTab) -> Void, + onTapLike: @escaping (Int) -> Void = { _ in }, + onTapComment: @escaping (Int) -> Void = { _ in }, + onTapPurchase: @escaping (CreatorChannelCommunityPostResponse) -> Void = { _ in } + ) { + self.communities = communities + self.onSelectTab = onSelectTab + self.onTapLike = onTapLike + self.onTapComment = onTapComment + self.onTapPurchase = onTapPurchase + } + + var body: some View { + if communities.isEmpty == false { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.community) + + VStack(alignment: .leading, spacing: SodaSpacing.s8) { + ForEach(visibleCommunities) { community in + CommunityPostCard( + postId: community.postId, + creatorNickname: community.creatorNickname, + creatorProfileImageUrl: community.creatorProfileUrl, + content: community.content, + imageUrl: community.imageUrl, + audioUrl: community.audioUrl, + price: community.price, + existOrdered: community.existOrdered, + createdAt: community.relativeTimeText(), + isLike: community.isLiked, + likeCount: community.likeCount, + commentCount: community.commentCount, + onTapLike: { + onTapLike(community.postId) + }, + onTapComment: { + onTapComment(community.postId) + }, + onTapPurchase: { + onTapPurchase(community) + } + ) + } + } + .padding(.horizontal, SodaSpacing.s14) + + Button { + onSelectTab(.community) + } label: { + Text(I18n.CreatorChannelHome.viewAll) + .appFont(size: 16, weight: .medium) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .overlay( + Capsule() + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + } + .buttonStyle(.plain) + .padding(.horizontal, SodaSpacing.s14) + } + .padding(.top, SodaSpacing.s20) + } + } + + private var visibleCommunities: [CreatorChannelCommunityPostResponse] { + Array(communities.prefix(homeVisibleCount)) + } +} + +private extension CreatorChannelCommunityPostResponse { + func relativeTimeText(now: Date = Date()) -> String { + DateParser.relativeTimeText(fromUTC: dateUtc, fallback: dateUtc, now: now) + } +} + +struct CreatorChannelCommunitySection_Previews: PreviewProvider { + private static let previewImageUrl = "https://picsum.photos/500/500" + + static var previews: some View { + CreatorChannelCommunitySection( + communities: (1...3).map { index in + CreatorChannelCommunityPostResponse( + postId: index, + creatorId: 10, + creatorNickname: "크리에이터 이름", + creatorProfileUrl: previewImageUrl, + imageUrl: index == 1 ? nil : previewImageUrl, + audioUrl: nil, + content: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", + price: index == 2 ? 30 : 0, + dateUtc: "2026-07-03T03:44:00Z", + existOrdered: false, + likeCount: 5, + commentCount: 6, + isLiked: false + ) + }, + onSelectTab: { _ in } + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index 01ec86e7..8583b572 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -63,6 +63,11 @@ struct CreatorChannelHomeView: View { onSelectTab: onSelectTab, onTapSeries: onTapSeries ) + + CreatorChannelCommunitySection( + communities: response.communities, + onSelectTab: onSelectTab + ) } .frame(maxWidth: .infinity, alignment: .leading) .background(Color.black) diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index 0f114056..65d1f105 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -466,7 +466,7 @@ ### Phase 12: 커뮤니티 섹션 -- [ ] **Task 12.1: 커뮤니티 섹션 구현** +- [x] **Task 12.1: 커뮤니티 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCommunitySection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift`