import Foundation import SwiftUI struct CreatorChannelScheduleSection: View { let schedules: [CreatorChannelScheduleResponse] let onTapSchedule: (CreatorActivityType, Int) -> Void init( schedules: [CreatorChannelScheduleResponse], onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in } ) { self.schedules = schedules self.onTapSchedule = onTapSchedule } var body: some View { if !schedules.isEmpty { VStack(alignment: .leading, spacing: SodaSpacing.s14) { SectionTitle(title: I18n.CreatorChannelHome.schedule) VStack(alignment: .leading, spacing: SodaSpacing.s4) { ForEach(Array(schedules.enumerated()), id: \.offset) { _, schedule in CreatorChannelScheduleItemView(schedule: schedule) { guard case .unknown = schedule.type else { onTapSchedule(schedule.type, schedule.targetId) return } } } } .background(alignment: .leading) { ScheduleTimelineLine() .padding(.leading, 3) .padding(.vertical, 52) .opacity(schedules.count > 1 ? 1 : 0) } .padding(.horizontal, SodaSpacing.s14) } .padding(.top, SodaSpacing.s20) } } } private struct CreatorChannelScheduleItemView: View { let schedule: CreatorChannelScheduleResponse let action: () -> Void var body: some View { HStack(alignment: .center, spacing: SodaSpacing.s14) { TimelineDotView(isHighlighted: schedule.isToday) Button(action: action) { HStack(alignment: .center, spacing: SodaSpacing.s12) { DateBoxView(schedule: schedule) VStack(alignment: .leading, spacing: SodaSpacing.s8) { 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.timeText) .appFont(size: 16, weight: .regular) .foregroundColor(Color.gray500) .lineLimit(1) .multilineTextAlignment(.trailing) } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(SodaSpacing.s14) .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } .buttonStyle(.plain) } } } 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: CreatorChannelScheduleResponse 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 CreatorChannelScheduleResponse { 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 timeText: String { 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 CreatorChannelScheduleSection_Previews: PreviewProvider { static var previews: some View { CreatorChannelScheduleSection( schedules: [ CreatorChannelScheduleResponse( scheduledAtUtc: ISO8601DateFormatter().string(from: Date()), title: "오늘 밤 라이브 방송", type: .live, targetId: 100 ), CreatorChannelScheduleResponse( scheduledAtUtc: "2026-07-12T12:30:00Z", title: "신규 오디오 콘텐츠 공개", type: .audio, targetId: 200 ) ] ) .padding(.vertical, SodaSpacing.s20) .background(Color.black) .previewLayout(.sizeThatFits) } }