diff --git a/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate index 7fd1356..9bfee64 100644 Binary files a/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate and b/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/myApp/Haru_Danim/IOS/ContentView.swift b/myApp/Haru_Danim/IOS/ContentView.swift index ddfb51a..6c33c7a 100644 --- a/myApp/Haru_Danim/IOS/ContentView.swift +++ b/myApp/Haru_Danim/IOS/ContentView.swift @@ -82,7 +82,7 @@ enum AppTab: String, CaseIterable, Identifiable { var label: String { switch self { - case .main: return "메인" + case .main: return "모음" case .action: return "행동" case .tag: return "꼬리표" case .goal: return "목표" diff --git a/myApp/Haru_Danim/IOS/Core/DebugSeed.swift b/myApp/Haru_Danim/IOS/Core/DebugSeed.swift index 677b5fc..962f064 100644 --- a/myApp/Haru_Danim/IOS/Core/DebugSeed.swift +++ b/myApp/Haru_Danim/IOS/Core/DebugSeed.swift @@ -96,6 +96,7 @@ enum DebugSeed { startDate: cal.date(byAdding: .day, value: -10, to: now)!, endDate: cal.date(byAdding: .day, value: 30, to: now)! ) + toeic.showsOnMain = true context.insert(toeic) let englishQuest = Quest(goal: toeic) englishQuest.targetAction = english diff --git a/myApp/Haru_Danim/IOS/Core/Models.swift b/myApp/Haru_Danim/IOS/Core/Models.swift index 6bee071..bd9ed60 100644 --- a/myApp/Haru_Danim/IOS/Core/Models.swift +++ b/myApp/Haru_Danim/IOS/Core/Models.swift @@ -188,6 +188,8 @@ final class Goal { var statusRaw: String = GoalStatus.inProgress.rawValue /// 목표 탭에서 하위 다짐 목록 접힘 여부 var isCollapsed: Bool = false + /// 모음 탭 상단에 진행 현황을 표시할 목표 (설정에서 선택, 최대 1개) + var showsOnMain: Bool = false var createdAt: Date = Date() @Relationship(deleteRule: .cascade, inverse: \Quest.goal) diff --git a/myApp/Haru_Danim/IOS/Localizable.xcstrings b/myApp/Haru_Danim/IOS/Localizable.xcstrings index cbb5855..d1ad832 100644 --- a/myApp/Haru_Danim/IOS/Localizable.xcstrings +++ b/myApp/Haru_Danim/IOS/Localizable.xcstrings @@ -119,6 +119,12 @@ }, "기록 직접 추가" : { + }, + "기록 필터" : { + + }, + "기록 확인" : { + }, "기록이 없어요" : { @@ -155,6 +161,9 @@ }, "꼬리표가 없어요" : { + }, + "꼬리표로 보기" : { + }, "끝" : { @@ -170,6 +179,9 @@ }, "누적 기록" : { + }, + "누적 횟수" : { + }, "다이나믹 아일랜드 · 잠금화면" : { @@ -250,6 +262,15 @@ }, "몇째 주" : { + }, + "모음" : { + + }, + "모음 탭" : { + + }, + "목록·타임테이블·통계 모두에 필터가 적용돼요." : { + }, "목표" : { @@ -265,6 +286,12 @@ }, "목표 수정" : { + }, + "목표 진행 현황" : { + + }, + "목표 진행 현황 표시" : { + }, "목표 추가" : { @@ -347,6 +374,12 @@ }, "선택" : { + }, + "선택한 꼬리표가 붙은 행동의 기록만 표시돼요." : { + + }, + "선택한 목표의 진행 현황이 모음 탭 상단에 표시돼요. 탭하면 목표 상세로 이동해요." : { + }, "선택한 탭(최대 %lld개)이 하단 탭바에 바로 표시되고, 나머지 탭은 ‘더보기’에서 열 수 있어요." : { @@ -551,6 +584,15 @@ }, "특정 시각 지정" : { + }, + "평균" : { + + }, + "평균 %@" : { + + }, + "표시 안 함" : { + }, "표시할 기록이 없어요" : { @@ -622,6 +664,9 @@ }, "행동 탭에서 추적할 행동을 추가해 보세요." : { + }, + "행동으로 보기 (여러 개 선택 가능)" : { + }, "현재 진행 중" : { diff --git a/myApp/Haru_Danim/IOS/Views/GoalViews.swift b/myApp/Haru_Danim/IOS/Views/GoalViews.swift index d31ff2a..62bbf76 100644 --- a/myApp/Haru_Danim/IOS/Views/GoalViews.swift +++ b/myApp/Haru_Danim/IOS/Views/GoalViews.swift @@ -255,6 +255,8 @@ struct GoalDetailView: View { editingQuest = quest } label: { QuestRow(quest: quest) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .buttonStyle(.plain) .swipeActions { diff --git a/myApp/Haru_Danim/IOS/Views/HistoryView.swift b/myApp/Haru_Danim/IOS/Views/HistoryView.swift index 9a94728..d3e142b 100644 --- a/myApp/Haru_Danim/IOS/Views/HistoryView.swift +++ b/myApp/Haru_Danim/IOS/Views/HistoryView.swift @@ -49,6 +49,8 @@ struct CountItem: Identifiable { struct HistoryView: View { @Query private var sessions: [TimeSession] @Query private var entries: [CountEntry] + @Query(sort: \Tag.sortOrder) private var tags: [Tag] + @Query(sort: \Action.sortOrder) private var allActions: [Action] @State private var selectedDayKey: Date = DayMath().dayKey(for: .now) @State private var mode: HistoryMode = { @@ -61,10 +63,21 @@ struct HistoryView: View { return .list }() @State private var timetableWeekly = false - @State private var statSpan: StatSpan = .day + @State private var statSpan: StatSpan = { + #if DEBUG + if let raw = UserDefaults.standard.string(forKey: "statSpan"), + let span = StatSpan(rawValue: raw) { + return span + } + #endif + return .day + }() @State private var editingSession: TimeSession? @State private var editingEntry: CountEntry? @State private var showingCalendar = false + @State private var showingFilter = false + @State private var filterTag: Tag? + @State private var filterActionIDs: Set = [] private var math: DayMath { DayMath() } private var dayRange: Range { math.dayRange(forKey: selectedDayKey) } @@ -81,6 +94,10 @@ struct HistoryView: View { .padding(.horizontal) .padding(.bottom, 8) + if isFiltering { + filterChip + } + switch mode { case .list: listView @@ -107,12 +124,93 @@ struct HistoryView: View { .background(AppTheme.background) .navigationTitle("기록") .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showingFilter = true + } label: { + Image(systemName: isFiltering + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease.circle") + } + } + } .sheet(item: $editingSession) { session in SessionEditorView(session: session) } .sheet(item: $editingEntry) { entry in CountEntryEditorView(entry: entry) } + .sheet(isPresented: $showingFilter) { + HistoryFilterSheet( + tags: tags, + actions: allActions, + filterTag: $filterTag, + filterActionIDs: $filterActionIDs + ) + } + } + + // MARK: 필터 (꼬리표 1개 또는 행동 여러 개) + + private var isFiltering: Bool { + filterTag != nil || !filterActionIDs.isEmpty + } + + private func passesFilter(_ action: Action) -> Bool { + if !filterActionIDs.isEmpty { + return filterActionIDs.contains(action.persistentModelID) + } + if let filterTag { + return action.tags.contains(filterTag) + } + return true + } + + private var filterLabel: String { + if !filterActionIDs.isEmpty { + let names = allActions + .filter { filterActionIDs.contains($0.persistentModelID) } + .map(\.name) + return names.count <= 2 + ? names.joined(separator: ", ") + : "\(names.prefix(2).joined(separator: ", ")) 외 \(names.count - 2)개" + } + if let filterTag { + return "#\(filterTag.name)" + } + return "" + } + + private var filterChip: some View { + HStack(spacing: 6) { + Image(systemName: "line.3.horizontal.decrease") + .font(.caption2.weight(.semibold)) + Text(filterLabel) + .font(.caption.weight(.semibold)) + .lineLimit(1) + Button { + withAnimation { + filterTag = nil + filterActionIDs = [] + } + } label: { + Image(systemName: "xmark.circle.fill") + .font(.caption) + .foregroundStyle(.secondary) + .padding(4) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + .foregroundStyle(AppTheme.green) + .padding(.leading, 10) + .padding(.trailing, 2) + .padding(.vertical, 4) + .background(AppTheme.green.opacity(0.12), in: Capsule()) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .padding(.bottom, 6) } // MARK: 날짜 이동 헤더 (날짜를 누르면 달력 팝업) @@ -123,6 +221,8 @@ struct HistoryView: View { moveDay(-1) } label: { Image(systemName: "chevron.left") + .frame(width: 44, height: 36) + .contentShape(Rectangle()) } Spacer() Button { @@ -162,6 +262,8 @@ struct HistoryView: View { moveDay(1) } label: { Image(systemName: "chevron.right") + .frame(width: 44, height: 36) + .contentShape(Rectangle()) } Button("오늘") { selectedDayKey = math.dayKey(for: .now) @@ -197,7 +299,7 @@ struct HistoryView: View { let now = Date.now var items: [SessionSegmentItem] = [] for session in sessions { - guard let action = session.action else { continue } + guard let action = session.action, passesFilter(action) else { continue } let end = session.endAt ?? now let lower = max(session.startAt, range.lowerBound) let upper = min(end, range.upperBound) @@ -213,6 +315,7 @@ struct HistoryView: View { .compactMap { entry in entry.action.map { CountItem(entry: entry, action: $0) } } + .filter { passesFilter($0.action) } .sorted { $0.entry.timestamp < $1.entry.timestamp } } @@ -255,6 +358,8 @@ struct HistoryView: View { .font(.subheadline.weight(.semibold)) .foregroundStyle(AppTheme.green) } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .buttonStyle(.plain) } @@ -292,6 +397,8 @@ struct HistoryView: View { .font(.subheadline.weight(.semibold)) .foregroundStyle(AppTheme.green) } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .buttonStyle(.plain) } @@ -508,6 +615,11 @@ struct StatsView: View { let stats = tagStats(in: spanRange) timeChartCard(stats) countChartCard(stats) + if statSpan != .day { + let points = trendPoints() + timeTrendCard(points) + countTrendCard(points) + } weeklyTrendCard } .padding() @@ -606,6 +718,122 @@ struct StatsView: View { } } + // MARK: 추세선 (주간/월간, 꺾은선 그래프 + 평균) + + private struct TrendPoint: Identifiable { + let dayKey: Date + let hours: Double + let count: Int + let cumulativeCount: Int + + var id: Date { dayKey } + } + + /// 선택 기간의 하루별 합산 시간과 누적 횟수 + private func trendPoints() -> [TrendPoint] { + var cumulative = 0 + return math.dayKeys(in: spanRange).map { key in + let range = math.dayRange(forKey: key) + let seconds = segments(range).reduce(0.0) { $0 + $1.duration } + let count = counts(range).reduce(0) { $0 + $1.entry.amount } + cumulative += count + return TrendPoint(dayKey: key, hours: seconds / 3600, count: count, cumulativeCount: cumulative) + } + } + + /// 평균 계산에 쓸 "이미 지나간(시작된)" 날 수 — 이번 주/이번 달의 미래 날짜로 평균이 희석되지 않게 + private func elapsedDayCount(_ points: [TrendPoint]) -> Int { + max(points.filter { math.dayRange(forKey: $0.dayKey).lowerBound <= .now }.count, 1) + } + + @ViewBuilder + private func timeTrendCard(_ points: [TrendPoint]) -> some View { + let totalHours = points.reduce(0.0) { $0 + $1.hours } + let avgHours = totalHours / Double(elapsedDayCount(points)) + chartCard("시간 추세 (하루 합산)") { + if totalHours == 0 { + emptyChartText + } else { + Chart { + ForEach(points) { point in + LineMark( + x: .value("날짜", point.dayKey, unit: .day), + y: .value("시간", point.hours) + ) + .foregroundStyle(AppTheme.green) + .interpolationMethod(.monotone) + .symbol(.circle) + } + RuleMark(y: .value("평균", avgHours)) + .foregroundStyle(AppTheme.yellow) + .lineStyle(StrokeStyle(lineWidth: 1.5, dash: [5, 4])) + .annotation(position: .top, alignment: .trailing) { + Text("평균 \(Format.durationShort(avgHours * 3600))") + .font(.caption2.weight(.semibold)) + .foregroundStyle(AppTheme.yellow) + } + } + .chartYAxisLabel("시간(h)") + .frame(height: 180) + averageRow( + total: Format.durationShort(totalHours * 3600), + average: Format.durationShort(avgHours * 3600) + ) + } + } + } + + @ViewBuilder + private func countTrendCard(_ points: [TrendPoint]) -> some View { + let totalCount = points.last?.cumulativeCount ?? 0 + let avgCount = Double(totalCount) / Double(elapsedDayCount(points)) + chartCard("횟수 추세 (누적)") { + if totalCount == 0 { + emptyChartText + } else { + Chart { + ForEach(points) { point in + LineMark( + x: .value("날짜", point.dayKey, unit: .day), + y: .value("누적 횟수", point.cumulativeCount) + ) + .foregroundStyle(AppTheme.green) + .interpolationMethod(.monotone) + .symbol(.circle) + } + } + .chartYAxisLabel("누적 횟수") + .frame(height: 180) + averageRow( + total: "\(totalCount)회", + average: String(format: "%.1f회", avgCount) + ) + } + } + } + + /// 그래프 아래 요약: 기간 합계 · 하루 평균 + private func averageRow(total: String, average: String) -> some View { + HStack(spacing: 0) { + summaryCell(label: "\(statSpan.label) 합계", value: total) + Divider().frame(height: 28) + summaryCell(label: "하루 평균", value: average) + } + .padding(.top, 4) + } + + private func summaryCell(label: String, value: String) -> some View { + VStack(spacing: 2) { + Text(label) + .font(.caption2) + .foregroundStyle(.secondary) + Text(value) + .font(.subheadline.weight(.semibold).monospacedDigit()) + .foregroundStyle(AppTheme.green) + } + .frame(maxWidth: .infinity) + } + /// 최근 7일 꼬리표별 시간 추이 (스택 막대) private var weeklyTrendCard: some View { let data = recentDailyStats() @@ -677,3 +905,107 @@ struct StatsView: View { .padding(.vertical, 24) } } + +// MARK: - 기록 필터 시트 (꼬리표 1개 또는 행동 다중 선택) + +struct HistoryFilterSheet: View { + @Environment(\.dismiss) private var dismiss + let tags: [Tag] + let actions: [Action] + @Binding var filterTag: Tag? + @Binding var filterActionIDs: Set + + var body: some View { + NavigationStack { + List { + Section { + selectableRow( + title: "전체 보기", + symbol: "square.grid.2x2", + color: AppTheme.green, + selected: filterTag == nil && filterActionIDs.isEmpty + ) { + filterTag = nil + filterActionIDs = [] + } + } + Section { + ForEach(tags) { tag in + selectableRow( + title: tag.name, + symbol: "tag.fill", + color: tag.color, + selected: filterTag == tag + ) { + filterTag = tag + filterActionIDs = [] + } + } + } header: { + Text("꼬리표로 보기") + } footer: { + Text("선택한 꼬리표가 붙은 행동의 기록만 표시돼요.") + } + Section { + ForEach(actions) { action in + selectableRow( + title: action.name, + symbol: action.symbolName, + color: action.color, + selected: filterActionIDs.contains(action.persistentModelID) + ) { + filterTag = nil + if filterActionIDs.contains(action.persistentModelID) { + filterActionIDs.remove(action.persistentModelID) + } else { + filterActionIDs.insert(action.persistentModelID) + } + } + } + } header: { + Text("행동으로 보기 (여러 개 선택 가능)") + } footer: { + Text("목록·타임테이블·통계 모두에 필터가 적용돼요.") + } + } + .scrollContentBackground(.hidden) + .background(AppTheme.background) + .navigationTitle("기록 필터") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("완료") { dismiss() } + } + } + } + } + + private func selectableRow( + title: String, + symbol: String, + color: Color, + selected: Bool, + onTap: @escaping () -> Void + ) -> some View { + Button(action: onTap) { + HStack(spacing: 10) { + Image(systemName: symbol) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: 28, height: 28) + .background(color, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + Text(title) + .foregroundStyle(.primary) + Spacer() + if selected { + Image(systemName: "checkmark") + .fontWeight(.semibold) + .foregroundStyle(AppTheme.green) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} diff --git a/myApp/Haru_Danim/IOS/Views/MainView.swift b/myApp/Haru_Danim/IOS/Views/MainView.swift index 201daa6..6a946e4 100644 --- a/myApp/Haru_Danim/IOS/Views/MainView.swift +++ b/myApp/Haru_Danim/IOS/Views/MainView.swift @@ -2,7 +2,7 @@ // MainView.swift // Haru_Danim // -// 메인 탭: 행동 버튼 그리드 + 현재 진행 중 영역 + 배치 편집 (CLAUDE.md §6.1) +// 모음 탭: 행동 버튼 그리드 + 현재 진행 중 영역 + 목표 진행 현황 + 배치 편집 (CLAUDE.md §6.1) // import SwiftUI @@ -14,6 +14,7 @@ struct MainView: View { @Query(sort: \Action.sortOrder) private var actions: [Action] @Query(filter: #Predicate { $0.endAt == nil }, sort: \TimeSession.startAt) private var runningSessions: [TimeSession] + @Query(filter: #Predicate { $0.showsOnMain }) private var pinnedGoals: [Goal] @AppStorage(SettingsKeys.gridColumns) private var gridColumns = 3 @AppStorage(SettingsKeys.minSessionSeconds) private var minSessionSeconds = 0 @@ -28,6 +29,7 @@ struct MainView: View { @State private var draggingAction: Action? @State private var settingsEditingAction: Action? @State private var recordsAction: Action? + @State private var recordsStartWithAdd = false @State private var deletingAction: Action? @State private var memoSession: TimeSession? @State private var memoEntry: CountEntry? @@ -37,6 +39,9 @@ struct MainView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { + if let goal = pinnedGoals.first, !isEditing { + GoalSummaryCard(goal: goal) + } if anyRunning && !isEditing { runningArea } @@ -52,7 +57,7 @@ struct MainView: View { .padding() } .background(AppTheme.background) - .navigationTitle(Text("하루다님", comment: "앱 이름")) + .navigationTitle("모음") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(isEditing ? "완료" : "배치 편집") { @@ -67,7 +72,7 @@ struct MainView: View { ActionEditorView(action: action) } .sheet(item: $recordsAction) { action in - ActionRecordsSheet(action: action) + ActionRecordsSheet(action: action, startWithAdd: recordsStartWithAdd) } .sheet(item: $memoSession) { session in SessionMemoSheet(session: session) @@ -174,6 +179,13 @@ struct MainView: View { base .contextMenu { Button { + recordsStartWithAdd = false + recordsAction = action + } label: { + Label("기록 확인", systemImage: "list.bullet.rectangle") + } + Button { + recordsStartWithAdd = true recordsAction = action } label: { Label( @@ -262,6 +274,79 @@ struct MainView: View { } } +// MARK: - 목표 진행 현황 카드 (설정에서 선택한 목표, 탭하면 목표 상세로 이동) + +struct GoalSummaryCard: View { + let goal: Goal + + var body: some View { + NavigationLink { + GoalDetailView(goal: goal) + } label: { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 10) { + Image(systemName: goal.symbolName) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .background(goal.color, in: RoundedRectangle(cornerRadius: 9, style: .continuous)) + VStack(alignment: .leading, spacing: 1) { + Text("목표 진행 현황") + .font(.caption2) + .foregroundStyle(.secondary) + Text(goal.title) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.primary) + .lineLimit(1) + } + Spacer() + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundStyle(.tertiary) + } + if goal.status == .inProgress, let progress = goal.dateProgress() { + VStack(alignment: .leading, spacing: 3) { + ProgressView(value: progress) + .tint(goal.color) + Text("기간 진행률 \(Format.percent(progress))") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + ForEach(goal.sortedQuests.prefix(3)) { quest in + questLine(quest) + } + } + .padding(14) + .frame(maxWidth: .infinity, alignment: .leading) + .background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) + .contentShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + } + .buttonStyle(.plain) + } + + /// 다짐 한 줄 요약: 현재 주기 진행률 게이지 + 퍼센트 + @ViewBuilder + private func questLine(_ quest: Quest) -> some View { + if let result = QuestProgress(quest: quest).current() { + let over = quest.direction == .atMost && result.value > result.target + HStack(spacing: 8) { + Text(quest.targetName) + .font(.caption) + .foregroundStyle(.primary) + .lineLimit(1) + .frame(width: 88, alignment: .leading) + ProgressView(value: result.ratio) + .tint(over ? .red : quest.targetColor) + Text(Format.percent(result.target > 0 ? result.value / result.target : 0)) + .font(.caption2.weight(.semibold).monospacedDigit()) + .foregroundStyle(over ? .red : .secondary) + .frame(width: 40, alignment: .trailing) + } + } + } +} + // MARK: - 측정 종료 메모 시트 struct SessionMemoSheet: View { diff --git a/myApp/Haru_Danim/IOS/Views/RecordEditors.swift b/myApp/Haru_Danim/IOS/Views/RecordEditors.swift index bf8030f..cded609 100644 --- a/myApp/Haru_Danim/IOS/Views/RecordEditors.swift +++ b/myApp/Haru_Danim/IOS/Views/RecordEditors.swift @@ -14,6 +14,8 @@ 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? @@ -51,6 +53,8 @@ struct ActionRecordsSheet: View { editingSession = session } label: { SessionRowLabel(session: session) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .buttonStyle(.plain) .swipeActions { @@ -69,6 +73,8 @@ struct ActionRecordsSheet: View { editingEntry = entry } label: { CountRowLabel(entry: entry) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) } .buttonStyle(.plain) .swipeActions { @@ -100,6 +106,9 @@ struct ActionRecordsSheet: View { CountAddView(action: action) } } + .onAppear { + if startWithAdd { showingAdd = true } + } } } } diff --git a/myApp/Haru_Danim/IOS/Views/SettingsView.swift b/myApp/Haru_Danim/IOS/Views/SettingsView.swift index 0206637..76f147b 100644 --- a/myApp/Haru_Danim/IOS/Views/SettingsView.swift +++ b/myApp/Haru_Danim/IOS/Views/SettingsView.swift @@ -6,8 +6,10 @@ // import SwiftUI +import SwiftData struct SettingsView: View { + @Query(sort: \Goal.createdAt) private var goals: [Goal] @AppStorage(SettingsKeys.theme) private var theme = "light" @AppStorage(SettingsKeys.language) private var language = AppLanguage.ko.rawValue @AppStorage(SettingsKeys.weekStartWeekday) private var weekStartWeekday = 2 @@ -33,6 +35,17 @@ struct SettingsView: View { } } + /// 모음 탭에 표시할 목표 선택 ↔ Goal.showsOnMain 플래그 변환 (최대 1개 유지) + private var pinnedGoalSelection: Binding { + Binding { + goals.first { $0.showsOnMain }?.persistentModelID + } set: { newValue in + for goal in goals { + goal.showsOnMain = goal.persistentModelID == newValue + } + } + } + var body: some View { Form { Section("화면") { @@ -55,6 +68,18 @@ struct SettingsView: View { } footer: { Text("선택한 탭(최대 \(TabBarConfig.maxVisible)개)이 하단 탭바에 바로 표시되고, 나머지 탭은 ‘더보기’에서 열 수 있어요.") } + Section { + Picker("목표 진행 현황 표시", selection: pinnedGoalSelection) { + Text("표시 안 함").tag(nil as PersistentIdentifier?) + ForEach(goals) { goal in + Text(goal.title).tag(Optional(goal.persistentModelID)) + } + } + } header: { + Text("모음 탭") + } footer: { + Text("선택한 목표의 진행 현황이 모음 탭 상단에 표시돼요. 탭하면 목표 상세로 이동해요.") + } Section { Picker("주 시작 요일", selection: $weekStartWeekday) { ForEach(1...7, id: \.self) { weekday in