// // RecordTabView.swift // HaruDanim // // Shows the tracking records (`TimeSession`s and `CountEntry`s) that occurred // on a user-selected date, as a single chronological list. // import SwiftUI import SwiftData struct RecordTabView: View { @Query(sort: \TimeSession.startTime, order: .reverse) private var sessions: [TimeSession] @Query(sort: \CountEntry.timestamp, order: .reverse) private var counts: [CountEntry] /// The day whose records are shown. Defaults to today. @State private var selectedDate: Date = .now /// Records that fall on `selectedDate`, merged and sorted newest first. private var recordsForDay: [RecordItem] { let calendar = Calendar.current let sessionItems = sessions .filter { calendar.isDate($0.startTime, inSameDayAs: selectedDate) } .map { RecordItem.session($0) } let countItems = counts .filter { calendar.isDate($0.timestamp, inSameDayAs: selectedDate) } .map { RecordItem.count($0) } return (sessionItems + countItems).sorted { $0.sortTime > $1.sortTime } } var body: some View { NavigationStack { List { Section { DatePicker( "날짜", selection: $selectedDate, displayedComponents: .date ) } Section("기록") { if recordsForDay.isEmpty { Text("이 날의 기록이 없어요.") .foregroundStyle(.secondary) } else { ForEach(recordsForDay) { item in switch item { case .session(let session): TimeSessionRow(session: session) case .count(let entry): CountEntryRow(entry: entry) } } } } } .navigationTitle("기록") } } } // MARK: - Record Item /// A single tracking record on a given day, either time- or count-based. private enum RecordItem: Identifiable { case session(TimeSession) case count(CountEntry) var id: UUID { switch self { case .session(let session): return session.id case .count(let entry): return entry.id } } /// The time used to order records within a day. var sortTime: Date { switch self { case .session(let session): return session.startTime case .count(let entry): return entry.timestamp } } } // MARK: - Rows private struct TimeSessionRow: View { let session: TimeSession var body: some View { HStack(spacing: 12) { TagColorDot(colorHex: session.action?.tags.first?.colorHex) VStack(alignment: .leading, spacing: 4) { Text(session.action?.name ?? "알 수 없는 행동") HStack(spacing: 4) { Text(session.startTime.formatted(date: .omitted, time: .shortened)) Text("–") if let endTime = session.endTime { Text(endTime.formatted(date: .omitted, time: .shortened)) } else { Text("진행 중") } } .font(.caption) .foregroundStyle(.secondary) } Spacer() Text(durationText) .font(.caption.monospacedDigit()) .foregroundStyle(Color.brandPrimary) } } /// Formatted elapsed time, or a running indicator when the session is open. private var durationText: String { guard let endTime = session.endTime else { return "진행 중" } return RecordFormatting.duration(from: session.startTime, to: endTime) } } private struct CountEntryRow: View { let entry: CountEntry var body: some View { HStack(spacing: 12) { TagColorDot(colorHex: entry.action?.tags.first?.colorHex) VStack(alignment: .leading, spacing: 4) { Text(entry.action?.name ?? "알 수 없는 행동") Text(entry.timestamp.formatted(date: .omitted, time: .standard)) .font(.caption) .foregroundStyle(.secondary) } Spacer() Text("\(entry.amount)회") .font(.caption.monospacedDigit()) .foregroundStyle(Color.brandPrimary) } } } // MARK: - Tag Color Dot private struct TagColorDot: View { let colorHex: String? var body: some View { Circle() .fill(colorHex.map { Color(hex: $0) } ?? Color.secondary.opacity(0.3)) .frame(width: 16, height: 16) } } // MARK: - Display Helpers private enum RecordFormatting { /// Formats an interval as `H시간 M분` (hours dropped when zero, at least `0분`). static func duration(from start: Date, to end: Date) -> String { let totalSeconds = max(0, Int(end.timeIntervalSince(start))) let hours = totalSeconds / 3600 let minutes = (totalSeconds % 3600) / 60 if hours > 0 { return "\(hours)시간 \(minutes)분" } return "\(minutes)분" } } #Preview { RecordTabView() .modelContainer(for: [Action.self, Tag.self, TimeSession.self, CountEntry.self], inMemory: true) }