403 lines
14 KiB
Swift
403 lines
14 KiB
Swift
//
|
||
// RecordEditors.swift
|
||
// Haru_Danim
|
||
//
|
||
// 기록 수동 입력·수정 시트 (메인 탭 롱프레스 / 기록 탭에서 사용, CLAUDE.md §6.1, §6.5)
|
||
//
|
||
|
||
import SwiftUI
|
||
import SwiftData
|
||
|
||
// MARK: - 행동별 기록 관리 시트 (롱프레스 메뉴에서 진입)
|
||
|
||
struct ActionRecordsSheet: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
let action: Action
|
||
/// true면 열리자마자 기록 추가 폼을 띄움 (롱프레스 '수동 입력' 메뉴용)
|
||
var startWithAdd = false
|
||
|
||
@State private var editingSession: TimeSession?
|
||
@State private var editingEntry: CountEntry?
|
||
@State private var showingAdd = false
|
||
|
||
private var recentSessions: [TimeSession] {
|
||
action.sessions.sorted { $0.startAt > $1.startAt }
|
||
}
|
||
|
||
private var recentEntries: [CountEntry] {
|
||
action.countEntries.sorted { $0.timestamp > $1.timestamp }
|
||
}
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
Section {
|
||
Button {
|
||
showingAdd = true
|
||
} label: {
|
||
Label(
|
||
action.trackingType == .time ? "기록 직접 추가" : "횟수 직접 추가",
|
||
systemImage: "plus.circle"
|
||
)
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
}
|
||
Section("기록 (최신순)") {
|
||
if action.trackingType == .time {
|
||
if recentSessions.isEmpty {
|
||
Text("기록이 없어요").foregroundStyle(.secondary)
|
||
}
|
||
ForEach(recentSessions) { session in
|
||
Button {
|
||
editingSession = session
|
||
} label: {
|
||
SessionRowLabel(session: session)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.contentShape(Rectangle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
.swipeActions {
|
||
Button("삭제", role: .destructive) {
|
||
context.delete(session)
|
||
LiveActivityManager.sync(context: context)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if recentEntries.isEmpty {
|
||
Text("기록이 없어요").foregroundStyle(.secondary)
|
||
}
|
||
ForEach(recentEntries) { entry in
|
||
Button {
|
||
editingEntry = entry
|
||
} label: {
|
||
CountRowLabel(entry: entry)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.contentShape(Rectangle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
.swipeActions {
|
||
Button("삭제", role: .destructive) {
|
||
context.delete(entry)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("\(action.name) 기록")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("완료") { dismiss() }
|
||
}
|
||
}
|
||
.sheet(item: $editingSession) { session in
|
||
SessionEditorView(session: session)
|
||
}
|
||
.sheet(item: $editingEntry) { entry in
|
||
CountEntryEditorView(entry: entry)
|
||
}
|
||
.sheet(isPresented: $showingAdd) {
|
||
if action.trackingType == .time {
|
||
SessionAddView(action: action)
|
||
} else {
|
||
CountAddView(action: action)
|
||
}
|
||
}
|
||
.onAppear {
|
||
if startWithAdd { showingAdd = true }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
struct SessionRowLabel: View {
|
||
let session: TimeSession
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
HStack {
|
||
Text(Format.fullDate(session.startAt))
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
Spacer()
|
||
if session.endAt == nil {
|
||
Text("진행 중")
|
||
.font(.caption.weight(.bold))
|
||
.foregroundStyle(AppTheme.yellow)
|
||
}
|
||
}
|
||
HStack {
|
||
Text("\(Format.time(session.startAt)) ~ \(session.endAt.map(Format.time) ?? "–")")
|
||
.font(.body)
|
||
Spacer()
|
||
Text(Format.durationShort(session.duration()))
|
||
.font(.subheadline.weight(.semibold))
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
if !session.note.isEmpty {
|
||
Label(session.note, systemImage: "note.text")
|
||
.font(.caption)
|
||
.foregroundStyle(AppTheme.yellow)
|
||
.lineLimit(2)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
struct CountRowLabel: View {
|
||
let entry: CountEntry
|
||
|
||
var body: some View {
|
||
HStack {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(Format.fullDate(entry.timestamp))
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
Text(Format.time(entry.timestamp))
|
||
.font(.body)
|
||
if !entry.note.isEmpty {
|
||
Label(entry.note, systemImage: "note.text")
|
||
.font(.caption)
|
||
.foregroundStyle(AppTheme.yellow)
|
||
.lineLimit(2)
|
||
}
|
||
}
|
||
Spacer()
|
||
Text("+\(entry.amount)")
|
||
.font(.subheadline.weight(.semibold))
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 시간 세션 수정
|
||
|
||
struct SessionEditorView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
let session: TimeSession
|
||
|
||
@State private var startAt: Date = .now
|
||
@State private var isFinished = true
|
||
@State private var endAt: Date = .now
|
||
@State private var note = ""
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
Section("시작") {
|
||
DatePicker("시작 시각", selection: $startAt)
|
||
}
|
||
Section("종료") {
|
||
Toggle("종료됨", isOn: $isFinished.animation())
|
||
if isFinished {
|
||
DatePicker("종료 시각", selection: $endAt, in: startAt...)
|
||
} else {
|
||
Text("종료 시각을 끄면 진행 중 상태가 돼요.")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
Section("메모") {
|
||
TextField("이 기록에 대한 메모 (선택)", text: $note, axis: .vertical)
|
||
.lineLimit(2...5)
|
||
}
|
||
Section {
|
||
Button("기록 삭제", role: .destructive) {
|
||
context.delete(session)
|
||
LiveActivityManager.sync(context: context)
|
||
dismiss()
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("시간 기록 수정")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("취소") { dismiss() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("저장") {
|
||
session.startAt = startAt
|
||
session.endAt = isFinished ? max(endAt, startAt) : nil
|
||
session.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
LiveActivityManager.sync(context: context)
|
||
dismiss()
|
||
}
|
||
}
|
||
}
|
||
.onAppear {
|
||
startAt = session.startAt
|
||
note = session.note
|
||
if let end = session.endAt {
|
||
isFinished = true
|
||
endAt = end
|
||
} else {
|
||
isFinished = false
|
||
endAt = .now
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 시간 세션 추가
|
||
|
||
struct SessionAddView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
let action: Action
|
||
|
||
@State private var startAt: Date = .now.addingTimeInterval(-3600)
|
||
@State private var endAt: Date = .now
|
||
@State private var note = ""
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
DatePicker("시작 시각", selection: $startAt)
|
||
DatePicker("종료 시각", selection: $endAt, in: startAt...)
|
||
Section("메모") {
|
||
TextField("이 기록에 대한 메모 (선택)", text: $note, axis: .vertical)
|
||
.lineLimit(2...5)
|
||
}
|
||
}
|
||
.navigationTitle("시간 기록 추가")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("취소") { dismiss() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("추가") {
|
||
let session = TimeSession(action: action, startAt: startAt, endAt: max(endAt, startAt))
|
||
session.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
context.insert(session)
|
||
dismiss()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 횟수 기록 수정
|
||
|
||
struct CountEntryEditorView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
let entry: CountEntry
|
||
|
||
@State private var timestamp: Date = .now
|
||
@State private var amount = 1
|
||
@State private var note = ""
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
DatePicker("기록 시각", selection: $timestamp)
|
||
Stepper(value: $amount, in: 1...9999) {
|
||
HStack {
|
||
Text("수량")
|
||
Spacer()
|
||
Text("\(amount)회").foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
Section("메모") {
|
||
TextField("이 기록에 대한 메모 (선택)", text: $note, axis: .vertical)
|
||
.lineLimit(2...5)
|
||
}
|
||
Section {
|
||
Button("기록 삭제 (증가 취소)", role: .destructive) {
|
||
context.delete(entry)
|
||
dismiss()
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("횟수 기록 수정")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("취소") { dismiss() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("저장") {
|
||
entry.timestamp = timestamp
|
||
entry.amount = amount
|
||
entry.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
dismiss()
|
||
}
|
||
}
|
||
}
|
||
.onAppear {
|
||
timestamp = entry.timestamp
|
||
amount = entry.amount
|
||
note = entry.note
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 횟수 기록 추가 (지금 시각 / 특정 시각 선택)
|
||
|
||
struct CountAddView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
let action: Action
|
||
|
||
@State private var amount = 1
|
||
@State private var useNow = true
|
||
@State private var timestamp: Date = .now
|
||
@State private var note = ""
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
Stepper(value: $amount, in: 1...9999) {
|
||
HStack {
|
||
Text("추가할 횟수")
|
||
Spacer()
|
||
Text("\(amount)회").foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
Section("기록 시각") {
|
||
Picker("기록 시각", selection: $useNow.animation()) {
|
||
Text("지금 시각").tag(true)
|
||
Text("특정 시각 지정").tag(false)
|
||
}
|
||
.pickerStyle(.segmented)
|
||
if !useNow {
|
||
DatePicker("시각", selection: $timestamp)
|
||
}
|
||
}
|
||
Section("메모") {
|
||
TextField("이 기록에 대한 메모 (선택)", text: $note, axis: .vertical)
|
||
.lineLimit(2...5)
|
||
}
|
||
}
|
||
.navigationTitle("횟수 추가")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("취소") { dismiss() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("추가") {
|
||
let entry = CountEntry(
|
||
action: action,
|
||
timestamp: useNow ? .now : timestamp,
|
||
amount: amount
|
||
)
|
||
entry.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
context.insert(entry)
|
||
dismiss()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|