// // SettingsView.swift // Haru_Danim // // 설정 탭 (CLAUDE.md §6.6) // 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 @AppStorage(SettingsKeys.dayStartMinutes) private var dayStartMinutes = 0 @AppStorage(SettingsKeys.liveActivityMode) private var liveActivityMode = LiveActivityMode.latest.rawValue @AppStorage(SettingsKeys.isPremium) private var isPremium = false @AppStorage(SettingsKeys.minSessionSeconds) private var minSessionSeconds = 0 @AppStorage(SettingsKeys.visibleTabs) private var visibleTabsRaw = TabBarConfig.defaultVisibleTabs @AppStorage(SettingsKeys.goalCardStyle) private var goalCardStyle = GoalCardStyle.perQuest.rawValue private var visibleTabs: [AppTab] { AppTab.visibleTabs(from: visibleTabsRaw) } /// 하루 시작 시간을 DatePicker와 연결하기 위한 변환 private var dayStartDate: Binding { Binding { let cal = Calendar.current return cal.date(byAdding: .minute, value: dayStartMinutes, to: cal.startOfDay(for: .now))! } set: { newValue in let cal = Calendar.current let comps = cal.dateComponents([.hour, .minute], from: newValue) dayStartMinutes = (comps.hour ?? 0) * 60 + (comps.minute ?? 0) } } /// 모음 탭에 표시할 목표 선택 ↔ 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("화면") { Picker("테마", selection: $theme) { Text("라이트").tag("light") Text("다크").tag("dark") } Picker("언어", selection: $language) { ForEach(AppLanguage.allCases) { lang in Text(lang.label).tag(lang.rawValue) } } } Section { ForEach(AppTab.allCases) { tab in tabToggleRow(tab) } } header: { Text("탭바 구성") } footer: { Text("선택한 탭(최대 \(TabBarConfig.maxVisible)개)이 하단 탭바에 바로 표시되고, 나머지 탭은 ‘더보기’에서 열 수 있어요.") } Section { Picker("목표 진행 현황 표시", selection: pinnedGoalSelection) { Text("표시 안 함").tag(nil as PersistentIdentifier?) ForEach(goals) { goal in Text(goal.title).tag(Optional(goal.persistentModelID)) } } if goals.contains(where: \.showsOnMain) { Picker("표시 방식", selection: $goalCardStyle) { ForEach(GoalCardStyle.allCases) { style in Text(style.label).tag(style.rawValue) } } } } header: { Text("모음 탭") } footer: { Text("선택한 목표의 진행 현황이 모음 탭 상단에 표시돼요. 탭하면 목표 상세로 이동해요. ‘다짐별로 각각’은 다짐(최대 3개)마다 하루·주간·월간 진행률을, ‘전체 다짐 합산’은 소속 다짐 전체의 평균 진행률을 한 줄로 보여줘요.") } Section { Picker("주 시작 요일", selection: $weekStartWeekday) { ForEach(1...7, id: \.self) { weekday in Text("\(Format.weekdayShort(weekday))요일").tag(weekday) } } DatePicker("하루 시작 시간", selection: dayStartDate, displayedComponents: .hourAndMinute) } header: { Text("날짜 기준") } footer: { Text("하루 시작 시간을 걸치는 기록은 통계에서 자동으로 날짜별로 나누어 계산돼요.") } Section { Picker("짧은 기록 무시", selection: $minSessionSeconds) { ForEach(MinSessionOption.choices, id: \.seconds) { option in Text(option.label).tag(option.seconds) } } } header: { Text("측정") } footer: { Text("설정한 시간보다 짧게 측정하고 종료한 기록은 저장하지 않아요. 버튼을 실수로 눌렀을 때 유용해요.") } Section { Picker("대표 시간 기준", selection: $liveActivityMode) { ForEach(LiveActivityMode.allCases) { mode in Text(mode.label).tag(mode.rawValue) } } } header: { Text("다이나믹 아일랜드 · 잠금화면") } footer: { Text("여러 행동을 동시에 추적 중일 때 대표로 표시할 시간이에요. (Live Activity 지원은 추후 추가 예정)") } Section("프리미엄") { NavigationLink { PremiumView() } label: { HStack { Label("프리미엄 기능", systemImage: "crown.fill") .foregroundStyle(AppTheme.yellow) Spacer() Text(isPremium ? "사용 중" : "무료 사용 중") .font(.caption) .foregroundStyle(.secondary) } } } Section { HStack { Text("버전") Spacer() Text("1.0").foregroundStyle(.secondary) } } } .scrollContentBackground(.hidden) .background(AppTheme.background) .navigationTitle("설정") } // MARK: 탭바 구성 private func tabToggleRow(_ tab: AppTab) -> some View { let isVisible = visibleTabs.contains(tab) let atMax = visibleTabs.count >= TabBarConfig.maxVisible let atMin = visibleTabs.count <= TabBarConfig.minVisible return Button { toggleTab(tab) } label: { HStack { Label { Text(tab.label) .foregroundStyle(.primary) } icon: { Image(systemName: tab.symbol) .foregroundStyle(AppTheme.green) } Spacer() if isVisible { Image(systemName: "checkmark") .foregroundStyle(AppTheme.green) .fontWeight(.semibold) } else { Text("더보기") .font(.caption) .foregroundStyle(.tertiary) } } .contentShape(Rectangle()) } .buttonStyle(.plain) .disabled((isVisible && atMin) || (!isVisible && atMax)) .opacity((isVisible && atMin) || (!isVisible && atMax) ? 0.5 : 1) } private func toggleTab(_ tab: AppTab) { var current = visibleTabs if let index = current.firstIndex(of: tab) { guard current.count > TabBarConfig.minVisible else { return } current.remove(at: index) } else { guard current.count < TabBarConfig.maxVisible else { return } current.append(tab) } // 스펙 탭 순서(메인→행동→꼬리표→목표→기록→설정) 유지 let ordered = AppTab.allCases.filter { current.contains($0) } visibleTabsRaw = ordered.map(\.rawValue).joined(separator: ",") } } // MARK: - 프리미엄 안내 struct PremiumView: View { @AppStorage(SettingsKeys.isPremium) private var isPremium = false var body: some View { List { Section { featureRow("infinity", "개수 제한 해제", "행동·목표·다짐을 무제한으로 만들 수 있어요.") featureRow("square.grid.2x2.fill", "홈·잠금화면 위젯", "위젯에서 바로 행동을 실행하고 달성률을 확인해요.") featureRow("icloud.fill", "아이패드 동기화", "iCloud로 기기 간 데이터가 동기화돼요.") featureRow("applewatch", "애플워치 앱", "워치에서 추적하고 컴플리케이션으로 확인해요.") } header: { Text("프리미엄으로 할 수 있는 것") } footer: { Text("무료: 행동 \(FreeLimits.actions)개 · 목표 \(FreeLimits.goals)개 · 목표당 다짐 \(FreeLimits.questsPerGoal)개까지") } if isPremium { Section("동기화") { Toggle("기기 간 동기화", isOn: .constant(false)) .disabled(true) Text("iCloud 동기화는 준비 중이에요.") .font(.caption) .foregroundStyle(.secondary) } } else { Section { Button { // StoreKit 결제는 Apple Developer Program 가입 후 연결 (CLAUDE.md §2.3) } label: { Label("프리미엄 구매 (준비 중)", systemImage: "cart.fill") } .disabled(true) } footer: { Text("결제 기능은 App Store 배포 준비와 함께 제공될 예정이에요.") } } #if DEBUG Section("개발용") { Toggle("프리미엄 상태 (테스트)", isOn: $isPremium) } #endif } .scrollContentBackground(.hidden) .background(AppTheme.background) .navigationTitle("프리미엄") .navigationBarTitleDisplayMode(.inline) .toolbar(.hidden, for: .tabBar) } private func featureRow(_ symbol: String, _ title: String, _ detail: String) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: symbol) .font(.system(size: 18)) .foregroundStyle(AppTheme.green) .frame(width: 30) VStack(alignment: .leading, spacing: 2) { Text(title).font(.body.weight(.semibold)) Text(detail).font(.caption).foregroundStyle(.secondary) } } .padding(.vertical, 2) } }