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(I18n.HomeFollowing.today) .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 I18n.HomeFollowing.onAirStatus } guard let scheduledDate else { return scheduledAtUtc } return ScheduleDateFormatter.time.string(from: scheduledDate) } } private extension CreatorActivityType { var scheduleTagTitle: String { switch self { case .live: return I18n.HomeFollowing.liveScheduleTag case .liveReplay: return I18n.HomeFollowing.liveReplayScheduleTag case .audio: return I18n.HomeFollowing.audioScheduleTag case .community: return I18n.HomeFollowing.communityScheduleTag 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) } }