feat(home): 팔로잉 스케줄 섹션을 추가한다
This commit is contained in:
@@ -267,6 +267,9 @@
|
|||||||
401C104A2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */; };
|
401C104A2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */; };
|
||||||
401C104B2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */; };
|
401C104B2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */; };
|
||||||
401C104C2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */; };
|
401C104C2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */; };
|
||||||
|
401C104D2FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift */; };
|
||||||
|
401C104E2FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift */; };
|
||||||
|
401C104F2FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.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 */; };
|
||||||
@@ -1808,6 +1811,7 @@
|
|||||||
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>"; };
|
401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingLiveSection.swift; sourceTree = "<group>"; };
|
||||||
401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingChatSection.swift; sourceTree = "<group>"; };
|
401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingChatSection.swift; sourceTree = "<group>"; };
|
||||||
|
401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingScheduleSection.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>"; };
|
||||||
@@ -2822,6 +2826,7 @@
|
|||||||
401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */,
|
401C10442FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift */,
|
||||||
401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */,
|
401C10482FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift */,
|
||||||
401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */,
|
401C10492FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift */,
|
||||||
|
401C10502FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -5493,6 +5498,7 @@
|
|||||||
401C10412FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
|
401C10412FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
|
||||||
401C10452FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
|
401C10452FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
|
||||||
401C104A2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */,
|
401C104A2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */,
|
||||||
|
401C104D2FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.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 */,
|
||||||
@@ -6260,6 +6266,7 @@
|
|||||||
401C10422FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
|
401C10422FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
|
||||||
401C10462FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
|
401C10462FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
|
||||||
401C104B2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */,
|
401C104B2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */,
|
||||||
|
401C104E2FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.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 */,
|
||||||
@@ -7057,6 +7064,7 @@
|
|||||||
401C10432FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
|
401C10432FEF500000A5BBA3 /* MainHomeFollowingCreatorSection.swift in Sources */,
|
||||||
401C10472FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
|
401C10472FEF700000A5BBA3 /* MainHomeFollowingLiveSection.swift in Sources */,
|
||||||
401C104C2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */,
|
401C104C2FEF900000A5BBA3 /* MainHomeFollowingChatSection.swift in Sources */,
|
||||||
|
401C104F2FEFA00000A5BBA3 /* MainHomeFollowingScheduleSection.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 */,
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MainHomeFollowingScheduleSection: View {
|
||||||
|
let monthlySchedules: [FollowingScheduleResponse]
|
||||||
|
let onTapSchedule: (CreatorActivityType, Int) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
monthlySchedules: [FollowingScheduleResponse],
|
||||||
|
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in }
|
||||||
|
) {
|
||||||
|
self.monthlySchedules = monthlySchedules
|
||||||
|
self.onTapSchedule = onTapSchedule
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if !monthlySchedules.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||||
|
SectionTitle(title: I18n.HomeFollowing.monthlyScheduleTitle)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s4) {
|
||||||
|
ForEach(monthlySchedules) { schedule in
|
||||||
|
MainHomeFollowingScheduleItemView(schedule: schedule) {
|
||||||
|
onTapSchedule(schedule.type, schedule.targetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(alignment: .leading) {
|
||||||
|
ScheduleTimelineLine()
|
||||||
|
.padding(.leading, 3)
|
||||||
|
.padding(.vertical, 52)
|
||||||
|
.opacity(monthlySchedules.count > 1 ? 1 : 0)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MainHomeFollowingScheduleItemView: View {
|
||||||
|
let schedule: FollowingScheduleResponse
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||||
|
TimelineDotView(isHighlighted: schedule.isToday || schedule.isOnAir)
|
||||||
|
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(alignment: .center, spacing: SodaSpacing.s12) {
|
||||||
|
DateBoxView(schedule: schedule)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||||
|
creatorInfo
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
||||||
|
Text(schedule.title)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
||||||
|
ActivityTypeTagView(type: schedule.type)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
|
Text(schedule.statusText)
|
||||||
|
.appFont(size: 16, weight: .regular)
|
||||||
|
.foregroundColor(schedule.isOnAir ? Color.soda400 : Color.gray500)
|
||||||
|
.lineLimit(1)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.padding(SodaSpacing.s14)
|
||||||
|
.padding(.leading, schedule.isOnAir ? SodaSpacing.s8 : 0)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
if schedule.isOnAir {
|
||||||
|
Color.soda400
|
||||||
|
.frame(width: SodaSpacing.s4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var creatorInfo: some View {
|
||||||
|
HStack(alignment: .center, spacing: SodaSpacing.s6) {
|
||||||
|
DownsampledKFImage(url: URL(string: schedule.creatorProfileImageUrl), size: CGSize(width: 20, height: 20))
|
||||||
|
.clipShape(Circle())
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text(schedule.creatorNickname)
|
||||||
|
.appFont(size: 12, weight: .regular)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ScheduleTimelineLine: View {
|
||||||
|
var body: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray700)
|
||||||
|
.frame(width: 2)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TimelineDotView: View {
|
||||||
|
let isHighlighted: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Circle()
|
||||||
|
.fill(isHighlighted ? Color.soda400 : Color.gray500)
|
||||||
|
.frame(width: SodaSpacing.s8, height: SodaSpacing.s8)
|
||||||
|
.frame(width: SodaSpacing.s8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DateBoxView: View {
|
||||||
|
let schedule: FollowingScheduleResponse
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .center, spacing: SodaSpacing.s4) {
|
||||||
|
if schedule.isToday {
|
||||||
|
Text("오늘")
|
||||||
|
.appFont(size: 22, weight: .bold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
} else {
|
||||||
|
Text(schedule.dayText)
|
||||||
|
.appFont(size: 22, weight: .bold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(schedule.weekdayText)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 53, height: 53)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ActivityTypeTagView: View {
|
||||||
|
let type: CreatorActivityType
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(type.scheduleTagTitle)
|
||||||
|
.appFont(size: 14, weight: .regular)
|
||||||
|
.foregroundColor(Color.gray100)
|
||||||
|
.padding(.horizontal, SodaSpacing.s4)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(Color.gray700)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s4, style: .continuous))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FollowingScheduleResponse {
|
||||||
|
var scheduledDate: Date? {
|
||||||
|
DateParser.parse(scheduledAtUtc)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isToday: Bool {
|
||||||
|
guard let scheduledDate else { return false }
|
||||||
|
return Calendar.current.isDateInToday(scheduledDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dayText: String {
|
||||||
|
guard let scheduledDate else { return scheduledAtUtc }
|
||||||
|
return ScheduleDateFormatter.day.string(from: scheduledDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var weekdayText: String {
|
||||||
|
guard let scheduledDate else { return "" }
|
||||||
|
return ScheduleDateFormatter.weekday.string(from: scheduledDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusText: String {
|
||||||
|
if isOnAir {
|
||||||
|
return "On Air"
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let scheduledDate else { return scheduledAtUtc }
|
||||||
|
return ScheduleDateFormatter.time.string(from: scheduledDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CreatorActivityType {
|
||||||
|
var scheduleTagTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .live:
|
||||||
|
return "라이브"
|
||||||
|
case .liveReplay:
|
||||||
|
return "다시보기"
|
||||||
|
case .audio:
|
||||||
|
return "오디오"
|
||||||
|
case .community:
|
||||||
|
return "커뮤니티"
|
||||||
|
case .unknown(let rawValue):
|
||||||
|
return rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ScheduleDateFormatter {
|
||||||
|
static let day: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "ko_KR")
|
||||||
|
formatter.dateFormat = "d"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let weekday: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "ko_KR")
|
||||||
|
formatter.dateFormat = "E"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let time: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "ko_KR")
|
||||||
|
formatter.dateFormat = "a hh:mm"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MainHomeFollowingScheduleSection_Previews: PreviewProvider {
|
||||||
|
private static let previewImageUrl = "https://picsum.photos/500/500"
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
MainHomeFollowingScheduleSection(
|
||||||
|
monthlySchedules: [
|
||||||
|
FollowingScheduleResponse(
|
||||||
|
scheduleId: "today-on-air",
|
||||||
|
creatorId: 1,
|
||||||
|
creatorProfileImageUrl: previewImageUrl,
|
||||||
|
creatorNickname: "크리에이터",
|
||||||
|
title: "오늘 밤 같이 이야기해요",
|
||||||
|
type: .live,
|
||||||
|
targetId: 100,
|
||||||
|
scheduledAtUtc: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
isOnAir: false
|
||||||
|
),
|
||||||
|
FollowingScheduleResponse(
|
||||||
|
scheduleId: "normal-schedule",
|
||||||
|
creatorId: 2,
|
||||||
|
creatorProfileImageUrl: previewImageUrl,
|
||||||
|
creatorNickname: "소다",
|
||||||
|
title: "신규 오디오 콘텐츠 공개",
|
||||||
|
type: .audio,
|
||||||
|
targetId: 200,
|
||||||
|
scheduledAtUtc: "2026-07-12T12:30:00Z",
|
||||||
|
isOnAir: false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.padding(.vertical, SodaSpacing.s20)
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ struct MainHomeFollowingView: View {
|
|||||||
let onTapFollowingAll: () -> Void
|
let onTapFollowingAll: () -> Void
|
||||||
let onTapChatTab: () -> Void
|
let onTapChatTab: () -> Void
|
||||||
let onTapChatRoom: (Int) -> Void
|
let onTapChatRoom: (Int) -> Void
|
||||||
|
let onTapSchedule: (CreatorActivityType, Int) -> Void
|
||||||
|
|
||||||
@StateObject private var viewModel = MainHomeFollowingViewModel()
|
@StateObject private var viewModel = MainHomeFollowingViewModel()
|
||||||
|
|
||||||
@@ -14,13 +15,15 @@ struct MainHomeFollowingView: View {
|
|||||||
onTapCreator: @escaping (Int) -> Void = { _ in },
|
onTapCreator: @escaping (Int) -> Void = { _ in },
|
||||||
onTapFollowingAll: @escaping () -> Void = {},
|
onTapFollowingAll: @escaping () -> Void = {},
|
||||||
onTapChatTab: @escaping () -> Void = {},
|
onTapChatTab: @escaping () -> Void = {},
|
||||||
onTapChatRoom: @escaping (Int) -> Void = { _ in }
|
onTapChatRoom: @escaping (Int) -> Void = { _ in },
|
||||||
|
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in }
|
||||||
) {
|
) {
|
||||||
self.onTapLive = onTapLive
|
self.onTapLive = onTapLive
|
||||||
self.onTapCreator = onTapCreator
|
self.onTapCreator = onTapCreator
|
||||||
self.onTapFollowingAll = onTapFollowingAll
|
self.onTapFollowingAll = onTapFollowingAll
|
||||||
self.onTapChatTab = onTapChatTab
|
self.onTapChatTab = onTapChatTab
|
||||||
self.onTapChatRoom = onTapChatRoom
|
self.onTapChatRoom = onTapChatRoom
|
||||||
|
self.onTapSchedule = onTapSchedule
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -44,6 +47,13 @@ struct MainHomeFollowingView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let monthlySchedules = viewModel.response?.monthlySchedules, !monthlySchedules.isEmpty {
|
||||||
|
MainHomeFollowingScheduleSection(
|
||||||
|
monthlySchedules: monthlySchedules,
|
||||||
|
onTapSchedule: onTapSchedule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
MainPlaceholderTabView(title: "팔로잉")
|
MainPlaceholderTabView(title: "팔로잉")
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct MainHomeView: View {
|
|||||||
let onTapFollowingAll: () -> Void
|
let onTapFollowingAll: () -> Void
|
||||||
let onTapChatTab: () -> Void
|
let onTapChatTab: () -> Void
|
||||||
let onTapChatRoom: (Int) -> Void
|
let onTapChatRoom: (Int) -> Void
|
||||||
|
let onTapFollowingSchedule: (CreatorActivityType, Int) -> Void
|
||||||
|
|
||||||
@State private var selectedTab: MainHomeTab = .recommendation
|
@State private var selectedTab: MainHomeTab = .recommendation
|
||||||
|
|
||||||
@@ -79,7 +80,8 @@ struct MainHomeView: View {
|
|||||||
onTapCreator: onTapCreator,
|
onTapCreator: onTapCreator,
|
||||||
onTapFollowingAll: onTapFollowingAll,
|
onTapFollowingAll: onTapFollowingAll,
|
||||||
onTapChatTab: onTapChatTab,
|
onTapChatTab: onTapChatTab,
|
||||||
onTapChatRoom: onTapChatRoom
|
onTapChatRoom: onTapChatRoom,
|
||||||
|
onTapSchedule: onTapFollowingSchedule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +120,8 @@ struct MainHomeView_Previews: PreviewProvider {
|
|||||||
onSelectFollowingTab: { true },
|
onSelectFollowingTab: { true },
|
||||||
onTapFollowingAll: {},
|
onTapFollowingAll: {},
|
||||||
onTapChatTab: {},
|
onTapChatTab: {},
|
||||||
onTapChatRoom: { _ in }
|
onTapChatRoom: { _ in },
|
||||||
|
onTapFollowingSchedule: { _, _ in }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ struct MainView: View {
|
|||||||
onSelectFollowingTab: handleFollowingTabSelection,
|
onSelectFollowingTab: handleFollowingTabSelection,
|
||||||
onTapFollowingAll: handleFollowingAllTap,
|
onTapFollowingAll: handleFollowingAllTap,
|
||||||
onTapChatTab: handleFollowingChatTabTap,
|
onTapChatTab: handleFollowingChatTabTap,
|
||||||
onTapChatRoom: handleFollowingChatRoomTap
|
onTapChatRoom: handleFollowingChatRoomTap,
|
||||||
|
onTapFollowingSchedule: handleFollowingScheduleTap
|
||||||
)
|
)
|
||||||
case .content:
|
case .content:
|
||||||
MainPlaceholderTabView(title: MainTab.content.title)
|
MainPlaceholderTabView(title: MainTab.content.title)
|
||||||
@@ -501,6 +502,21 @@ struct MainView: View {
|
|||||||
appState.setAppStep(step: .chatRoom(id: roomId))
|
appState.setAppStep(step: .chatRoom(id: roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleFollowingScheduleTap(type: CreatorActivityType, targetId: Int) {
|
||||||
|
guard targetId > 0 else { return }
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .live, .liveReplay:
|
||||||
|
handleRecommendationLiveTap(roomId: targetId)
|
||||||
|
case .audio:
|
||||||
|
handleRecommendationContentTap(contentId: targetId)
|
||||||
|
case .community:
|
||||||
|
handleRecommendationCommunityTap(creatorId: targetId)
|
||||||
|
case .unknown:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func handleFollowingTabSelection() -> Bool {
|
private func handleFollowingTabSelection() -> Bool {
|
||||||
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty else {
|
guard !trimmed.isEmpty else {
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
|
|
||||||
### Phase 7: 이달의 스케줄 섹션
|
### Phase 7: 이달의 스케줄 섹션
|
||||||
|
|
||||||
- [ ] **Task 7.1: 이달의 스케줄 섹션 생성**
|
- [x] **Task 7.1: 이달의 스케줄 섹션 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingScheduleSection.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingScheduleSection.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
- 실행 명령: `rg "struct MainHomeFollowingScheduleSection|monthlyScheduleTitle|FollowingScheduleResponse|scheduledAtUtc|isOnAir|Color.soda400|CreatorActivityType|onTapSchedule|오늘" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingScheduleSection.swift`
|
- 실행 명령: `rg "struct MainHomeFollowingScheduleSection|monthlyScheduleTitle|FollowingScheduleResponse|scheduledAtUtc|isOnAir|Color.soda400|CreatorActivityType|onTapSchedule|오늘" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingScheduleSection.swift`
|
||||||
- 기대 결과: 스케줄 카드 표시, On Air 강조, schedule tap callback이 확인된다.
|
- 기대 결과: 스케줄 카드 표시, On Air 강조, schedule tap callback이 확인된다.
|
||||||
|
|
||||||
- [ ] **Task 7.2: 스케줄 상세 진입 연결**
|
- [x] **Task 7.2: 스케줄 상세 진입 연결**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
|
- 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
|
||||||
- 수정: `SodaLive/Sources/V2/Main/MainView.swift`
|
- 수정: `SodaLive/Sources/V2/Main/MainView.swift`
|
||||||
@@ -527,3 +527,11 @@
|
|||||||
- 2026-07-01: `rg "MainHomeFollowingChatSection.swift|MainHomeFollowingChatSection.swift in Sources" SodaLive.xcodeproj/project.pbxproj`로 신규 Swift 파일의 Xcode 프로젝트 등록을 확인했다.
|
- 2026-07-01: `rg "MainHomeFollowingChatSection.swift|MainHomeFollowingChatSection.swift in Sources" SodaLive.xcodeproj/project.pbxproj`로 신규 Swift 파일의 Xcode 프로젝트 등록을 확인했다.
|
||||||
- 2026-07-01: `if rg "localhost:3845|unread" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingChatSection.swift; then exit 1; else true; fi`를 실행해 Figma localhost asset과 별도 unread 표시가 들어가지 않았음을 확인했다.
|
- 2026-07-01: `if rg "localhost:3845|unread" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingChatSection.swift; then exit 1; else true; fi`를 실행해 Figma localhost asset과 별도 unread 표시가 들어가지 않았음을 확인했다.
|
||||||
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 Phase 6 구현 후 빌드 성공을 확인했다. 기존 duplicate build file/Crashlytics dSYM/AppIntents warning은 남아 있으나 컴파일 오류는 없었다.
|
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 Phase 6 구현 후 빌드 성공을 확인했다. 기존 duplicate build file/Crashlytics dSYM/AppIntents warning은 남아 있으나 컴파일 오류는 없었다.
|
||||||
|
- 2026-07-01: Figma `24:5703`, `24:5706`, `24:5707`을 확인해 Phase 7 이달의 스케줄 섹션을 구현했다. 카드 `Color.gray900`, 14pt radius/padding, 53x53 날짜 영역, 20pt 프로필, 16pt 제목/상태, gray700 활동 tag를 반영했고, 오늘/On Air dot 및 On Air 상태/좌측 accent는 `Color.soda400`, 일반 schedule dot/line/status는 gray 계열로 분리했다.
|
||||||
|
- 2026-07-01: `MainHomeFollowingView -> MainHomeView -> MainView.handleFollowingScheduleTap(type:targetId:)` 경로로 스케줄 tap callback을 연결했다. `.live`/`.liveReplay`은 기존 live 입장, `.audio`는 콘텐츠 상세, `.community`는 기존 정책에 따라 `creatorCommunityAll(creatorId:)`, `.unknown`은 no-op 처리했다.
|
||||||
|
- 2026-07-01: `SodaLive.xcodeproj/project.pbxproj`에 `MainHomeFollowingScheduleSection.swift` 파일 참조와 Sources build file을 추가해 Xcode navigator와 target compile sources에서 확인 가능하도록 등록했다.
|
||||||
|
- 2026-07-01: `rg "MainHomeFollowingScheduleSection.swift|MainHomeFollowingScheduleSection.swift in Sources|onTapFollowingSchedule|onTapSchedule|monthlySchedules|CreatorActivityType|contentDetail|creatorCommunityAll|enterLiveRoom|On Air|a hh:mm|오늘|Color.soda400|Color.gray500|localhost:3845" SodaLive/Sources/V2/Main/Home/Following SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift SodaLive.xcodeproj/project.pbxproj`로 Phase 7 UI, 라우팅, 프로젝트 등록, Figma localhost asset 미사용을 확인했다.
|
||||||
|
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`는 기존 DerivedData `build.db` lock으로 실패했으나, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase7" build`로 재실행해 빌드 성공을 확인했다. 기존 duplicate build file/run script/AppIntents/dependency scan warning은 남아 있으나 컴파일 오류는 없었다.
|
||||||
|
- 2026-07-01: 사용자 피드백에 따라 Figma `24:5703`의 왼쪽 스케줄 timeline을 재확인했다. 기존 item별 `TimelineGuideView`는 카드 사이 gap에서 line이 끊겨 보이고 dot 위치가 Figma보다 카드에 가까웠으므로, list 전체 overlay의 `ScheduleTimelineLine`과 item별 `TimelineDotView` 구조로 변경하고 item gap을 4pt로 맞췄다.
|
||||||
|
- 2026-07-01: `rg "VStack\(alignment: \.leading, spacing: SodaSpacing\.s4\)|ScheduleTimelineLine|TimelineDotView|padding\(\.vertical, 52\)|opacity\(monthlySchedules\.count > 1|HStack\(alignment: \.center, spacing: SodaSpacing\.s14\)|localhost:3845" SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingScheduleSection.swift`로 연속 line/dot 구조와 Figma localhost asset 미사용을 확인했다.
|
||||||
|
- 2026-07-01: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug -derivedDataPath "/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/SodaLiveDerivedDataPhase7" build`를 실행해 timeline 수정 후 빌드 성공을 확인했다. 기존 warning은 남아 있으나 컴파일 오류는 없었다.
|
||||||
|
|||||||
Reference in New Issue
Block a user