From 0e358d73c3675f0128b723cede2013e012ce51d7 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 3 Jul 2026 02:48:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=ED=98=84=EC=9E=AC=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=EC=84=B9=EC=85=98=EC=9D=84=20=EC=97=B0?= =?UTF-8?q?=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 | 8 + SodaLive/Sources/App/AppDelegate.swift | 6 + SodaLive/Sources/App/SodaLiveApp.swift | 10 ++ SodaLive/Sources/Splash/SplashView.swift | 6 + .../CreatorChannelCurrentLiveSection.swift | 140 ++++++++++++++++++ .../Home/CreatorChannelHomeView.swift | 20 ++- docs/20260701_크리에이터_채널_홈/plan-task.md | 3 +- 7 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 4a7d7613..8df1c0ed 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ CC0101072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */; }; CC0102072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */; }; CC0103072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */; }; + CC0101142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */; }; + CC0102142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */; }; + CC0103142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.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 */; }; @@ -1746,6 +1749,7 @@ CC0100112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTitleBar.swift; sourceTree = ""; }; CC0100122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHeaderSection.swift; sourceTree = ""; }; CC0100132FF0100000A5BBA3 /* CreatorChannelTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTabBar.swift; sourceTree = ""; }; + CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelCurrentLiveSection.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 = ""; }; @@ -2665,6 +2669,7 @@ isa = PBXGroup; children = ( CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */, + CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */, ); path = Components; sourceTree = ""; @@ -5599,6 +5604,7 @@ CC0101042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, CC0101062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0101072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, + CC0101142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6396,6 +6402,7 @@ CC0102042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, CC0102062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0102072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, + CC0102142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7209,6 +7216,7 @@ CC0103042FF0100000A5BBA3 /* CreatorChannelHomeRepository.swift in Sources */, CC0103062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0103072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, + CC0103142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, diff --git a/SodaLive/Sources/App/AppDelegate.swift b/SodaLive/Sources/App/AppDelegate.swift index 8f5a15f4..48fc0df4 100644 --- a/SodaLive/Sources/App/AppDelegate.swift +++ b/SodaLive/Sources/App/AppDelegate.swift @@ -25,6 +25,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private var subscription = Set() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + guard !Self.isRunningForPreviews else { return true } + FirebaseApp.configure() LoginManager.shared.setup(channelID: LINE_CHANNEL_ID, universalLinkURL: nil) Notifly.initialize(projectId: NOTIFLY_PROJECT_ID, username: NOTIFLY_USERNAME, password: NOTIFLY_PASSWORD) @@ -50,6 +52,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + + private static var isRunningForPreviews: Bool { + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // Print full message. diff --git a/SodaLive/Sources/App/SodaLiveApp.swift b/SodaLive/Sources/App/SodaLiveApp.swift index 44c96258..a8aae839 100644 --- a/SodaLive/Sources/App/SodaLiveApp.swift +++ b/SodaLive/Sources/App/SodaLiveApp.swift @@ -29,8 +29,14 @@ struct SodaLiveApp: App { configureImageCache() // 앱 시작 직후, 초기 네트워크 요청도 올바른 언어 헤더를 갖도록 동기 초기화 LanguageHeaderProvider.initialize() + guard !Self.isRunningForPreviews else { return } + KakaoSDK.initSDK(appKey: KAKAO_APP_KEY) } + + private static var isRunningForPreviews: Bool { + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + } private func configureImageCache() { // Kingfisher 전역 캐시 상한 설정 @@ -50,6 +56,8 @@ struct SodaLiveApp: App { ContentView(canPgPaymentViewModel: canPgPaymentViewModel) .environment(\.locale, languageEnvironment.locale) .task { + guard !Self.isRunningForPreviews else { return } + await LanguageContainer.service.bootstrap() } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in @@ -62,6 +70,8 @@ struct SodaLiveApp: App { ImageCache.default.clearMemoryCache() } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + guard !Self.isRunningForPreviews else { return } + UIApplication.shared.applicationIconBadgeNumber = 0 AppsFlyerLib.shared().start() diff --git a/SodaLive/Sources/Splash/SplashView.swift b/SodaLive/Sources/Splash/SplashView.swift index df034991..156fce37 100644 --- a/SodaLive/Sources/Splash/SplashView.swift +++ b/SodaLive/Sources/Splash/SplashView.swift @@ -86,6 +86,8 @@ struct SplashView: View { } private func fetchLastestVersion() { + guard !isRunningForPreviews else { return } + let remoteConfig = RemoteConfig.remoteConfig() let configSettings = RemoteConfigSettings() configSettings.minimumFetchInterval = 60 @@ -163,6 +165,10 @@ struct SplashView: View { nextAppStep() } } + + private var isRunningForPreviews: Bool { + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + } } #Preview { diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift new file mode 100644 index 00000000..c8aa67d8 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift @@ -0,0 +1,140 @@ +import SwiftUI + +struct CreatorChannelCurrentLiveSection: View { + let currentLive: CreatorChannelLiveResponse? + let onTapLive: (Int) -> Void + + init( + currentLive: CreatorChannelLiveResponse?, + onTapLive: @escaping (Int) -> Void = { _ in } + ) { + self.currentLive = currentLive + self.onTapLive = onTapLive + } + + var body: some View { + if let currentLive { + CreatorChannelCurrentLiveCard(live: currentLive) { + onTapLive(currentLive.liveId) + } + .padding(.horizontal, SodaSpacing.s20) + } + } +} + +private struct CreatorChannelCurrentLiveCard: View { + let live: CreatorChannelLiveResponse + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(alignment: .center, spacing: 0) { + VStack(alignment: .leading, spacing: SodaSpacing.s4) { + HStack(alignment: .center, spacing: SodaSpacing.s8) { + LiveTagView() + + Text(live.relativeStartedAtText()) + .appFont(size: 14, weight: .regular) + .foregroundColor(Color.gray700) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) + } + + Text(live.title) + .appFont(size: 18, weight: .bold) + .foregroundColor(.black) + .lineLimit(1) + .truncationMode(.tail) + } + .frame(width: 262, alignment: .leading) + + Spacer(minLength: 0) + + priceBadge + .frame(height: 78) + .layoutPriority(1) + } + .padding(.leading, SodaSpacing.s24) + .padding(.trailing, SodaSpacing.s14) + .frame(maxWidth: .infinity, minHeight: 78, alignment: .leading) + .background(cardBackground) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + + private var cardBackground: LinearGradient { + LinearGradient( + colors: [Color(hex: "00FBE2"), Color(hex: "62CFFF")], + startPoint: .leading, + endPoint: .trailing + ) + } + + private var priceBadge: some View { + ZStack { + HStack(spacing: 2) { + Image("ic_bar_cash") + .resizable() + .scaledToFit() + + Text(live.priceText) + .appFont(size: 14, weight: .regular) + .foregroundColor(.white) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) + } + .padding(.horizontal, SodaSpacing.s6) + .padding(.vertical, SodaSpacing.s4) + .background(Color.black.opacity(0.4)) + .clipShape(Capsule()) + } + } +} + +private struct LiveTagView: View { + var body: some View { + HStack(alignment: .center, spacing: SodaSpacing.s4) { + Circle() + .fill(Color.red400) + .frame(width: 8, height: 8) + + Text(I18n.HomeFollowing.liveTag) + .appFont(size: 12, weight: .regular) + .foregroundColor(.white) + .lineLimit(1) + } + .padding(.horizontal, SodaSpacing.s6) + .padding(.vertical, 2) + .frame(height: 18) + .background(Color.black) + .clipShape(Capsule()) + } +} + +private extension CreatorChannelLiveResponse { + func relativeStartedAtText(now: Date = Date()) -> String { + DateParser.relativeTimeText(fromUTC: beginDateTimeUtc, fallback: beginDateTimeUtc, now: now) + } + + var priceText: String { + price > 0 ? price.comma() : I18n.LiveReservation.Item.free + } +} + +struct CreatorChannelCurrentLiveSection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelCurrentLiveSection( + currentLive: CreatorChannelLiveResponse( + liveId: 1, + title: "라이브 제목", + coverImageUrl: "https://picsum.photos/300/300", + beginDateTimeUtc: "2026-07-01T00:30:00Z", + price: 300, + isAdult: true + ) + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index 17fb2e7e..5ccf219e 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -3,12 +3,26 @@ import SwiftUI struct CreatorChannelHomeView: View { let response: CreatorChannelHomeResponse let onSelectTab: (CreatorChannelTab) -> Void + let onTapLive: (Int) -> Void + + init( + response: CreatorChannelHomeResponse, + onSelectTab: @escaping (CreatorChannelTab) -> Void, + onTapLive: @escaping (Int) -> Void = { _ in } + ) { + self.response = response + self.onSelectTab = onSelectTab + self.onTapLive = onTapLive + } var body: some View { - ScrollView { - Color.clear - .frame(height: 1) + VStack(alignment: .leading, spacing: 0) { + CreatorChannelCurrentLiveSection( + currentLive: response.currentLive, + onTapLive: onTapLive + ) } + .frame(maxWidth: .infinity, alignment: .leading) .background(Color.black) } } diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index a8670c02..287eebd3 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -335,7 +335,7 @@ ### Phase 5: 현재 라이브 섹션 -- [ ] **Task 5.1: 현재 라이브 섹션 구현** +- [x] **Task 5.1: 현재 라이브 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` @@ -619,6 +619,7 @@ - 2026-07-02: 사용자 확인 사항을 반영해 title bar, 크리에이터 프로필 header, tab-bar를 `CreatorChannelView` 공통 shell로 분리하고, `CreatorChannelHomeView`는 tab-bar 아래 홈 탭 콘텐츠만 담당하도록 PRD와 계획을 갱신했다. - 2026-07-02: 이미 완료된 Phase 1~2 기록은 보존하고, 후속 구조 정리를 위해 Task 2.4 `공통 CreatorChannel shell로 구조 정리`를 추가했다. - 2026-07-02: 사용자 확인 사항을 반영해 title bar Figma URL 3종과 사용할 아이콘 asset(`ic_new_bar_back`, `ic_new_follow`, `ic_new_following`, `ic_new_more`, `ic_bar_bell`, `ic_bar_bell_fill`)을 Task 3.1 및 최종 회귀 검색 기준에 추가했다. +- 2026-07-03: Phase 5 현재 라이브 섹션을 구현했다. Figma `290:8950` 기준으로 LIVE pill, 시작 시간, 제목, 커버 이미지/placeholder, 유료 캔 배지, 성인 표시를 반영하고 `CreatorChannelHomeView`에 `currentLive` optional 기반으로 연결했다. - 2026-07-03: Phase 4를 완료해 `CreatorChannelView`에서 홈 탭은 `CreatorChannelHomeView`, 홈 외 탭은 `CreatorChannelPlaceholderTabView(title: selectedTab.title)`로 분기하고, 홈 섹션 전체보기용 tab 선택 closure가 `.audio`, `.series`, `.community`, `.fanTalk`, `.donation`으로 전환되도록 연결했다. - 2026-07-02: 기존 `UserProfile` 구현을 확인해 title bar 팔로우/팔로우 취소/알림 설정/알림 설정 취소를 `UserRepository.creatorFollow(creatorId:follow:notify:)`로 처리하고, `ic_bar_bell`/`ic_bar_bell_fill` 터치 시 각각 `notify: true`/`notify: false` 호출이 일어나도록 계획에 반영했다. - 2026-07-03: Task 3.4 구현을 완료했다. `CreatorChannelView`에서 geometry preference로 tab-bar 위치를 추적하고, sticky 기준을 디바이스 최상단이 아닌 title bar 하단(`safeAreaInsets.top + 56`)으로 계산하도록 변경했다. `rg "PreferenceKey|GeometryReader|ZStack|overlay|backgroundProgress|Color\\.clear|opacity|isTabBarSticky|clamp|safeAreaInsets|CreatorChannelTitleBar|CreatorChannelTabBar" ...` 기준 키워드를 확인했고, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`가 `BUILD SUCCEEDED`로 완료됐다.