feat(home): 팔로잉 On Air 섹션을 추가한다

This commit is contained in:
Yu Sung
2026-07-01 10:36:03 +09:00
parent 2a23a341c0
commit 5761d6083b
5 changed files with 165 additions and 1 deletions

View File

@@ -261,6 +261,9 @@
401C10412FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */; }; 401C10412FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */; };
401C10422FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */; }; 401C10422FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */; };
401C10432FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */; }; 401C10432FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */; };
401C10452FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */; };
401C10462FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */; };
401C10472FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */; };
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; }; 40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; }; 40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; }; 40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; };
@@ -1800,6 +1803,7 @@
401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingRepository.swift; sourceTree = "<group>"; }; 401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingRepository.swift; sourceTree = "<group>"; };
401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingViewModel.swift; sourceTree = "<group>"; }; 401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingViewModel.swift; sourceTree = "<group>"; };
401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingCreatorSection.swift; sourceTree = "<group>"; }; 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingCreatorSection.swift; sourceTree = "<group>"; };
401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingLiveSection.swift; sourceTree = "<group>"; };
40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; }; 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; };
40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; }; 40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; }; 40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; };
@@ -2812,6 +2816,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */, 401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */,
401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -5481,6 +5486,7 @@
401C10132FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */, 401C10132FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */,
401C10142FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */, 401C10142FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */,
401C10412FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */, 401C10412FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
401C10452FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */, 409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */,
40975CA32B572298004E2F1B /* LiveRoomOverlayStrokeTextButton.swift in Sources */, 40975CA32B572298004E2F1B /* LiveRoomOverlayStrokeTextButton.swift in Sources */,
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */, 40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */,
@@ -6246,6 +6252,7 @@
401C10232FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */, 401C10232FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */,
401C10242FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */, 401C10242FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */,
401C10422FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */, 401C10422FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
401C10462FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */, B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */,
CA65AD0D677B4A8E9CAC0E15 /* MainTab.swift in Sources */, CA65AD0D677B4A8E9CAC0E15 /* MainTab.swift in Sources */,
A0643C829BE7377DC09112CB /* MainTabBarButton.swift in Sources */, A0643C829BE7377DC09112CB /* MainTabBarButton.swift in Sources */,
@@ -7041,6 +7048,7 @@
401C10332FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */, 401C10332FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */,
401C10342FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */, 401C10342FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */,
401C10432FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */, 401C10432FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
401C10472FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */, ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */,
33BA3AB731849284E62AE04B /* MainTab.swift in Sources */, 33BA3AB731849284E62AE04B /* MainTab.swift in Sources */,
B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */, B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */,

View File

@@ -0,0 +1,136 @@
import SwiftUI
struct MainHomeFollowingLiveSection: View {
let onAirLives: [FollowingLiveResponse]
let onTapLive: (Int) -> Void
init(
onAirLives: [FollowingLiveResponse],
onTapLive: @escaping (Int) -> Void = { _ in }
) {
self.onAirLives = onAirLives
self.onTapLive = onTapLive
}
var body: some View {
if !onAirLives.isEmpty {
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
SectionTitle(title: I18n.HomeFollowing.onAirTitle)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(alignment: .top, spacing: SodaSpacing.s8) {
ForEach(onAirLives) { live in
MainHomeFollowingLiveItemView(live: live) {
onTapLive(live.liveId)
}
}
}
.padding(.leading, SodaSpacing.s14)
}
}
}
}
}
private struct MainHomeFollowingLiveItemView: View {
let live: FollowingLiveResponse
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(alignment: .center, spacing: SodaSpacing.s8) {
DownsampledKFImage(url: URL(string: live.creatorProfileImageUrl), size: CGSize(width: 75, height: 75))
.clipShape(Circle())
VStack(alignment: .leading, spacing: SodaSpacing.s4) {
HStack(alignment: .center, spacing: SodaSpacing.s8) {
LiveTagView()
Text(live.relativeStartedAtText())
.appFont(size: 14, weight: .regular)
.foregroundColor(Color.gray500)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
Text(live.title)
.appFont(size: 18, weight: .bold)
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
Text(live.creatorNickname)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color.gray500)
.lineLimit(1)
.truncationMode(.tail)
}
.frame(width: 149, alignment: .leading)
}
.padding(.leading, SodaSpacing.s12)
.padding(.trailing, 22)
.padding(.vertical, SodaSpacing.s12)
.background(Color.gray900)
.clipShape(Capsule())
.overlay {
Capsule()
.stroke(Color(hex: "62CFFF"), lineWidth: 2)
}
}
.buttonStyle(.plain)
}
}
private struct LiveTagView: View {
var body: some View {
HStack(alignment: .center, spacing: SodaSpacing.s4) {
Circle()
.fill(Color.red400)
.frame(width: 8, height: 8)
Text("LIVE")
.appFont(size: 12, weight: .regular)
.foregroundColor(.white)
.lineLimit(1)
}
.padding(.horizontal, SodaSpacing.s4)
.padding(.vertical, 2)
.frame(height: 18)
.background(Color.black)
.clipShape(Capsule())
}
}
private extension FollowingLiveResponse {
func relativeStartedAtText(now: Date = Date()) -> String {
DateParser.relativeTimeText(fromUTC: startedAtUtc, fallback: startedAtUtc, now: now)
}
}
struct MainHomeFollowingLiveSection_Previews: PreviewProvider {
private static let previewImageUrl = "https://placehold.co/500"
static var previews: some View {
MainHomeFollowingLiveSection(
onAirLives: [
FollowingLiveResponse(
liveId: 1,
creatorProfileImageUrl: previewImageUrl,
creatorNickname: "크리에이터",
title: "오늘 밤 같이 이야기해요",
startedAtUtc: "2026-07-01T00:30:00Z"
),
FollowingLiveResponse(
liveId: 2,
creatorProfileImageUrl: previewImageUrl,
creatorNickname: "소다",
title: "신규 콘텐츠 제작 비하인드",
startedAtUtc: "2026-07-01T00:10:00Z"
)
]
)
.padding(.vertical, SodaSpacing.s20)
.background(Color.black)
.previewLayout(.sizeThatFits)
}
}

View File

@@ -1,15 +1,18 @@
import SwiftUI import SwiftUI
struct MainHomeFollowingView: View { struct MainHomeFollowingView: View {
let onTapLive: (Int) -> Void
let onTapCreator: (Int) -> Void let onTapCreator: (Int) -> Void
let onTapFollowingAll: () -> Void let onTapFollowingAll: () -> Void
@StateObject private var viewModel = MainHomeFollowingViewModel() @StateObject private var viewModel = MainHomeFollowingViewModel()
init( init(
onTapLive: @escaping (Int) -> Void = { _ in },
onTapCreator: @escaping (Int) -> Void = { _ in }, onTapCreator: @escaping (Int) -> Void = { _ in },
onTapFollowingAll: @escaping () -> Void = {} onTapFollowingAll: @escaping () -> Void = {}
) { ) {
self.onTapLive = onTapLive
self.onTapCreator = onTapCreator self.onTapCreator = onTapCreator
self.onTapFollowingAll = onTapFollowingAll self.onTapFollowingAll = onTapFollowingAll
} }
@@ -22,6 +25,11 @@ struct MainHomeFollowingView: View {
onTapAll: onTapFollowingAll onTapAll: onTapFollowingAll
) )
MainHomeFollowingLiveSection(
onAirLives: viewModel.response?.onAirLives ?? [],
onTapLive: onTapLive
)
MainPlaceholderTabView(title: "팔로잉") MainPlaceholderTabView(title: "팔로잉")
} }
.onAppear { .onAppear {

View File

@@ -73,6 +73,7 @@ struct MainHomeView: View {
MainHomeRankingView(onTapCreator: onTapCreator) MainHomeRankingView(onTapCreator: onTapCreator)
case .following: case .following:
MainHomeFollowingView( MainHomeFollowingView(
onTapLive: onTapLive,
onTapCreator: onTapCreator, onTapCreator: onTapCreator,
onTapFollowingAll: onTapFollowingAll onTapFollowingAll: onTapFollowingAll
) )

View File

@@ -236,7 +236,7 @@
### Phase 5: On Air 섹션 ### Phase 5: On Air 섹션
- [ ] **Task 5.1: On Air 섹션 생성** - [x] **Task 5.1: On Air 섹션 생성**
- 대상 파일: - 대상 파일:
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingLiveSection.swift` - 생성: `SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingLiveSection.swift`
- 작업 내용: - 작업 내용:
@@ -508,3 +508,14 @@
- 2026-07-01: `rg "StateObject private var viewModel = MainHomeFollowingViewModel|onAppear|hasLoaded|fetchFollowing|response\?\.followingCreators|isLoading|message|MainPlaceholderTabView" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`로 Phase 4 정적 요구사항을 확인했다. - 2026-07-01: `rg "StateObject private var viewModel = MainHomeFollowingViewModel|onAppear|hasLoaded|fetchFollowing|response\?\.followingCreators|isLoading|message|MainPlaceholderTabView" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`로 Phase 4 정적 요구사항을 확인했다.
- 2026-07-01: `rg "onSelectFollowingTab|Binding\(|selectedTab = \.following|setAppStep\(step: \.login\)|TextTabBar" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift`로 비로그인 팔로잉 탭 guard 흐름이 유지됨을 확인했다. - 2026-07-01: `rg "onSelectFollowingTab|Binding\(|selectedTab = \.following|setAppStep\(step: \.login\)|TextTabBar" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift`로 비로그인 팔로잉 탭 guard 흐름이 유지됨을 확인했다.
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 Phase 4 API 호출 연결 후 빌드 성공을 확인했다. 기존 AppIntents metadata extraction warning은 남아 있으나 컴파일 오류는 없었다. - 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 Phase 4 API 호출 연결 후 빌드 성공을 확인했다. 기존 AppIntents metadata extraction warning은 남아 있으나 컴파일 오류는 없었다.
- 2026-07-01: Phase 5 구현을 진행해 `MainHomeFollowingLiveSection`을 추가하고, `FollowingLiveResponse`의 프로필 이미지/닉네임/라이브 제목/`startedAtUtc` 상대 시간 표시와 `onTapLive(liveId)` callback을 연결했다.
- 2026-07-01: `rg "struct MainHomeFollowingLiveSection|onAirTitle|FollowingLiveResponse|startedAtUtc|DateParser|onTapLive|DownsampledKFImage|ScrollView\(\.horizontal|!onAirLives\.isEmpty" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingLiveSection.swift`로 Phase 5 정적 요구사항을 확인했다.
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 Phase 5 신규 Swift 파일 컴파일과 빌드 성공을 확인했다. 기존 Crashlytics dSYM/AppIntents metadata extraction warning은 남아 있으나 컴파일 오류는 없었다.
- 2026-07-01: 사용자 요청에 따라 Phase 11까지 기다리지 않고 `MainHomeFollowingView``MainHomeFollowingLiveSection`을 조기 조립했다. `viewModel.response?.onAirLives ?? []`를 전달하고, `MainHomeView`의 기존 `onTapLive` callback을 팔로잉 On Air 섹션까지 연결했다.
- 2026-07-01: `SodaLive.xcodeproj/project.pbxproj``MainHomeFollowingLiveSection.swift` 파일 참조와 Sources build file을 추가해 Xcode navigator와 target compile sources에서 바로 확인 가능하도록 등록했다.
- 2026-07-01: `rg "MainHomeFollowingLiveSection.swift|MainHomeFollowingLiveSection.swift in Sources|MainHomeFollowingLiveSection\(" SodaLive.xcodeproj/project.pbxproj SodaLive/Sources/V2/Main/Home`로 Xcode 프로젝트 등록과 화면 조립을 확인했다.
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 재실행해 프로젝트 등록 후 빌드 성공을 확인했다. 기존 duplicate build file/run script/Crashlytics dSYM/AppIntents warning은 남아 있으나 컴파일 오류는 없었다.
- 2026-07-01: Figma `24:5693`, `24:5696`을 확인해 On Air 섹션을 캡슐형 라이브 아이템으로 수정했다. `75x75` 프로필, 검정 `LIVE` pill, `#62CFFF` 2pt 캡슐 테두리, `14/18/14pt` 텍스트 계층, leading 14/item gap 8 간격을 반영했다.
- 2026-07-01: `rg "CGSize\(width: 75, height: 75\)|Color\(hex: \"62CFFF\"\)|Text\(\"LIVE\"\)|frame\(height: 18\)|padding\(\.trailing, 22\)|padding\(\.leading, SodaSpacing\.s14\)|spacing: SodaSpacing\.s8|appFont\(size: 18, weight: \.bold\)|appFont\(size: 14, weight: \.regular\)" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingLiveSection.swift`로 Figma 핵심 수치 반영을 확인했다.
- 2026-07-01: `rg "localhost:3845|img_live|frame\(width: 50, height: 18\)|CGSize\(width: 62, height: 62\)|RoundedRectangle" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingLiveSection.swift` 결과가 없어 Figma localhost asset 미사용과 기존 배지/카드 형태 제거를 확인했다.
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 Figma 반영 후 빌드 성공을 확인했다. 기존 Crashlytics dSYM/AppIntents warning은 남아 있으나 컴파일 오류는 없었다.