mycode/myApp/HaruDanim/IOS/Views/RecordTabView.swift
songyc macbook c35f50fa73 feat: add Goal(목표), Quest(다짐), and history tracking features
- Implement Goal(목표) and Quest(다짐) functionality
- Add a new feature for tracking and recording history
- TODO: Fix an issue in 횟수측정 Action(행동) where count and time are recorded but not reflected in the icon
2026-07-07 02:37:16 +09:00

184 lines
5.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)
}