From 3f24788c290e970c92ebf4006eeb0ef114c4d1a7 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 3 Jul 2026 17:23:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=86=8C=EA=B0=9C,=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99,=20SNS=20=EC=84=B9=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=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 | 24 ++++++ SodaLive/Sources/I18n/I18n.swift | 20 +++++ .../CreatorChannelActivitySection.swift | 69 +++++++++++++++ .../CreatorChannelIntroduceSection.swift | 37 ++++++++ .../Components/CreatorChannelSnsSection.swift | 85 +++++++++++++++++++ .../Home/CreatorChannelHomeView.swift | 6 ++ docs/20260701_크리에이터_채널_홈/plan-task.md | 6 +- 7 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 20ca6b2c..c643c7d7 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -52,6 +52,15 @@ CC01011D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift */; }; CC01021D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift */; }; CC01031D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift */; }; + CC01011E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */; }; + CC01021E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */; }; + CC01031E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */; }; + CC01011F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */; }; + CC01021F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */; }; + CC01031F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */; }; + CC0101272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; }; + CC0102272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */; }; + CC0103272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.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 */; }; @@ -1786,6 +1795,9 @@ CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSeriesSection.swift; sourceTree = ""; }; CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelCommunitySection.swift; sourceTree = ""; }; CC01001D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkSection.swift; sourceTree = ""; }; + CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelIntroduceSection.swift; sourceTree = ""; }; + CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelActivitySection.swift; sourceTree = ""; }; + CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSnsSection.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 = ""; }; @@ -2715,6 +2727,9 @@ CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */, CC01001C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift */, CC01001D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift */, + CC01001E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift */, + CC01001F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift */, + CC0100272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift */, ); path = Components; sourceTree = ""; @@ -5659,6 +5674,9 @@ CC01011B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, CC01011C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */, CC01011D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift in Sources */, + CC01011E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */, + CC01011F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */, + CC0101272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6466,6 +6484,9 @@ CC01021B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, CC01021C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */, CC01021D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift in Sources */, + CC01021E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */, + CC01021F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */, + CC0102272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7289,6 +7310,9 @@ CC01031B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, CC01031C2FF0100000A5BBA3 /* CreatorChannelCommunitySection.swift in Sources */, CC01031D2FF0100000A5BBA3 /* CreatorChannelFanTalkSection.swift in Sources */, + CC01031E2FF0100000A5BBA3 /* CreatorChannelIntroduceSection.swift in Sources */, + CC01031F2FF0100000A5BBA3 /* CreatorChannelActivitySection.swift in Sources */, + CC0103272FF0100000A5BBA3 /* CreatorChannelSnsSection.swift in Sources */, CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 7aa83d35..05d7b125 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3196,6 +3196,26 @@ If you block this user, the following features will be restricted. static func followerCount(_ count: String) -> String { pick(ko: "팔로워 \(count)명", en: "\(count) followers", ja: "フォロワー\(count)人") } + + static func liveCountValue(_ count: String) -> String { + pick(ko: "\(count)회", en: "\(count)", ja: "\(count)回") + } + + static func liveDurationHoursValue(_ count: String) -> String { + pick(ko: "\(count)시간", en: "\(count) hours", ja: "\(count)時間") + } + + static func liveContributorCountValue(_ count: String) -> String { + pick(ko: "\(count)명", en: "\(count) people", ja: "\(count)人") + } + + static func audioContentCountValue(_ count: String) -> String { + pick(ko: "\(count)개", en: "\(count)", ja: "\(count)件") + } + + static func seriesCountValue(_ count: String) -> String { + pick(ko: "\(count)개", en: "\(count)", ja: "\(count)件") + } } enum HomeRecommendation { diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift new file mode 100644 index 00000000..28a42c0c --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift @@ -0,0 +1,69 @@ +import SwiftUI + +struct CreatorChannelActivitySection: View { + let activity: CreatorChannelActivityResponse + + var body: some View { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.activity) + + VStack(alignment: .leading, spacing: SodaSpacing.s8) { + activityRow(title: I18n.CreatorChannelHome.debut, value: activity.debutText) + activityRow(title: I18n.CreatorChannelHome.liveCount, value: I18n.CreatorChannelHome.liveCountValue(activity.liveCount.comma())) + activityRow(title: I18n.CreatorChannelHome.liveDurationHours, value: I18n.CreatorChannelHome.liveDurationHoursValue(activity.liveDurationHours.comma())) + activityRow(title: I18n.CreatorChannelHome.liveContributorCount, value: I18n.CreatorChannelHome.liveContributorCountValue(activity.liveContributorCount.comma())) + activityRow(title: I18n.CreatorChannelHome.audioContentCount, value: I18n.CreatorChannelHome.audioContentCountValue(activity.audioContentCount.comma())) + activityRow(title: I18n.CreatorChannelHome.seriesCount, value: I18n.CreatorChannelHome.seriesCountValue(activity.seriesCount.comma())) + } + .padding(.horizontal, SodaSpacing.s20) + } + .padding(.top, SodaSpacing.s20) + } + + private func activityRow(title: String, value: String) -> some View { + HStack(alignment: .top, spacing: SodaSpacing.s8) { + Text(title) + .appFont(size: 16, weight: .medium) + .foregroundColor(Color.gray500) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(value) + .appFont(size: 16, weight: .medium) + .foregroundColor(.white) + .lineLimit(1) + .multilineTextAlignment(.trailing) + } + } +} + +private extension CreatorChannelActivityResponse { + var debutText: String { + let dDayText = dday.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let debutDateUtc, + let debutDate = DateParser.parse(debutDateUtc), + !dDayText.isEmpty else { + return dDayText + } + + return "\(debutDate.convertDateFormat(dateFormat: "yyyy.MM.dd"))(\(dDayText))" + } +} + +struct CreatorChannelActivitySection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelActivitySection( + activity: CreatorChannelActivityResponse( + debutDateUtc: "2026-06-11T00:00:00Z", + dday: "D+1", + liveCount: 719, + liveDurationHours: 1839, + liveContributorCount: 2189, + audioContentCount: 281, + seriesCount: 8 + ) + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift new file mode 100644 index 00000000..4da25b44 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct CreatorChannelIntroduceSection: View { + let introduce: String + + private var trimmedIntroduce: String { + introduce.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var body: some View { + if !trimmedIntroduce.isEmpty { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.introduce) + + Text(trimmedIntroduce) + .appFont(size: 16, weight: .regular) + .foregroundColor(.white) + .lineSpacing(4) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, SodaSpacing.s20) + } + .padding(.top, SodaSpacing.s20) + } + } +} + +struct CreatorChannelIntroduceSection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelIntroduceSection( + introduce: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분" + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift new file mode 100644 index 00000000..f9be6eee --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift @@ -0,0 +1,85 @@ +import SwiftUI + +struct CreatorChannelSnsSection: View { + let sns: CreatorChannelSnsResponse + + @Environment(\.openURL) private var openURL + + private var snsItems: [CreatorChannelSnsItem] { + [ + CreatorChannelSnsItem(iconName: "ic_sns_instagram", url: sns.instagramUrl), + CreatorChannelSnsItem(iconName: "ic_sns_youtube", url: sns.youtubeUrl), + CreatorChannelSnsItem(iconName: "ic_sns_x", url: sns.xurl), + CreatorChannelSnsItem(iconName: "ic_sns_kakao", url: sns.kakaoOpenChatUrl), + CreatorChannelSnsItem(iconName: "ic_sns_fancimm", url: sns.fancimmUrl) + ] + .compactMap { item in + guard normalizedUrl(item.url) != nil else { return nil } + return item + } + } + + var body: some View { + if !snsItems.isEmpty { + VStack(alignment: .leading, spacing: 0) { + SectionTitle(title: I18n.CreatorChannelHome.sns) + + HStack(alignment: .center, spacing: SodaSpacing.s16) { + ForEach(snsItems) { item in + Button { + if let url = normalizedUrl(item.url) { + openURL(url) + } + } label: { + Image(item.iconName) + .resizable() + .scaledToFill() + .frame(width: 52, height: 52) + .clipShape(Circle()) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, SodaSpacing.s20) + } + .padding(.top, SodaSpacing.s20) + } + } + + private func normalizedUrl(_ urlString: String) -> URL? { + let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines) + + guard !trimmed.isEmpty else { + return nil + } + + if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { + return URL(string: trimmed) + } + + return URL(string: "https://\(trimmed)") + } +} + +private struct CreatorChannelSnsItem: Identifiable { + let iconName: String + let url: String + + var id: String { iconName } +} + +struct CreatorChannelSnsSection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelSnsSection( + sns: CreatorChannelSnsResponse( + instagramUrl: "https://instagram.com/voiceon", + fancimmUrl: "https://fancimm.com", + xurl: "https://x.com/voiceon", + youtubeUrl: "https://youtube.com", + kakaoOpenChatUrl: "https://open.kakao.com" + ) + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index 364c6785..4abd93c3 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -77,6 +77,12 @@ struct CreatorChannelHomeView: View { fanTalk: response.fanTalk, onSelectTab: onSelectTab ) + + CreatorChannelIntroduceSection(introduce: response.introduce) + + CreatorChannelActivitySection(activity: response.activity) + + CreatorChannelSnsSection(sns: response.sns) } .frame(maxWidth: .infinity, alignment: .leading) .background(Color.black) diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index 1183af66..bafb1fad 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -496,7 +496,7 @@ ### Phase 14: 소개 섹션 -- [ ] **Task 14.1: 소개 섹션 구현** +- [x] **Task 14.1: 소개 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelIntroduceSection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` @@ -509,7 +509,7 @@ ### Phase 15: 활동 섹션 -- [ ] **Task 15.1: 활동 섹션 구현** +- [x] **Task 15.1: 활동 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelActivitySection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` @@ -524,7 +524,7 @@ ### Phase 16: SNS 섹션 -- [ ] **Task 16.1: SNS 섹션 구현** +- [x] **Task 16.1: SNS 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSnsSection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift`