2026-07-23 사용성 감사(A그룹 5건 + 지시 추가분)를 한 번에 반영.
[테마] '시스템 설정 따름' 추가 + 기본값 light→system. "system"은 스킴을
강제하지 않아(preferredColorScheme nil) 앱·위젯('앱 일치' 옵션,
WidgetThemeOption.scheme: ColorScheme?)·DEBUG 위젯 미리보기까지
시스템 라이트/다크를 그대로 따른다
[온보딩] 첫 실행 3장 소개(OnboardingView — 행동→목표·다짐→정리).
데이터가 하나도 없고 본 적 없을 때만 1회(건너뛰기 포함 재표시 없음,
onboarding.done), 시드·마케팅 촬영 플로우는 자동 건너뜀.
설정 → 지원 '앱 소개 다시 보기'로 재열람
[일기 잠금] 설정 → 일기(iPad 전용 표시) 토글 — DiaryLock이
.deviceOwnerAuthentication으로 Face ID/Touch ID/Optic ID+암호 폴백을
기기별 분기 없이 처리. 일기 탭 진입 게이트(DiaryLockGateView),
백그라운드 재잠금, 토글 변경 시 인증 요구, 잠글 수단 없는 기기는
통과(영구 잠김 방지). NSFaceIDUsageDescription 추가(Info.plist+
InfoPlist.xcstrings ko/en/ja)
[통계] 이전 기간 비교 카드(맨 위) — 총 시간·횟수를 어제/지난주/지난달과
비교(▲▼%, 이전 기록 없으면 상태 문구). 이전 기간은 관계 기반
Aggregator로 직접 집계, 필터 반영. 화면 전용(내보내기 미포함 의도)
[스플래시] 1.2→0.7초 단축 (하루에도 여러 번 여는 앱)
[워치] 행동 실행 탭 시 WKInterfaceDevice.play(.click) 햅틱 — 화면을
안 보고 탭해도 접수 확인
[컴플리케이션] rectangular 빈 상태에 해결 안내("목표 편집에서
'애플워치에서 보기'를 켜면 나타나요") — 옵트인 발견성 보완
[문구] 행동 편집기 꼬리표 색 규칙(여러 개면 가장 먼저 만든 꼬리표),
새로고침 버튼 accessibilityHint
- 도움말 3주제 추가(새로고침 버튼·일기 잠금·이전 기간과 비교)
- 신규 문구 31키 en/ja 완역 + 워치 카탈로그 2키, missing/stale 0
- CLAUDE.md §6·§6.6·§6.7·§6.8·§10·§11·§14 갱신, DEBUG 인자
-showOnboarding·-diaryLockScreen 추가
- 검증: Debug/워치/Store 빌드, 온보딩·설정·비교 카드·잠금 게이트·
컴플리케이션 스크린샷
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
3.7 KiB
Swift
82 lines
3.7 KiB
Swift
//
|
|
// Haru_DanimApp.swift
|
|
// Haru_Danim
|
|
//
|
|
// Created by 송예찬 on 7/7/26.
|
|
//
|
|
|
|
import AppIntents
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
@main
|
|
struct Haru_DanimApp: App {
|
|
/// App Group 컨테이너의 공유 DB (위젯·App Intents와 공용, 프리미엄이면 CloudKit 연동)
|
|
private let container: ModelContainer
|
|
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
|
|
init() {
|
|
// 실결제 권한 제공자 주입 (다른 무엇보다 먼저 — shared 최초 접근 전에 교체)
|
|
PremiumManager.bootstrap(provider: StoreKitEntitlementProvider())
|
|
SettingsMigration.runIfNeeded()
|
|
container = DataStore.shared
|
|
DataStore.ensureUniqueEntityIDs(context: container.mainContext)
|
|
// 배치·노출 등 기기별 UI 설정: 구버전에 모델(@Model)로 저장하던 값을 로컬로 1회 이관
|
|
LocalPrefs.adoptLegacyModelValuesIfNeeded(context: container.mainContext)
|
|
// 시리 단축어/위젯 인텐트가 앱 프로세스에서 실행될 때 Live Activity·워치도 갱신되도록 주입
|
|
IntentHooks.afterMutation = { context in
|
|
LiveActivityManager.sync(context: context)
|
|
WatchSyncManager.shared.pushSnapshot()
|
|
}
|
|
WatchSyncManager.shared.activate()
|
|
// CloudKit 원격 변경(다른 기기·위젯 인텐트의 쓰기)이 오면 위젯·워치·라이브 액티비티 갱신
|
|
CloudSyncMonitor.shared.start(container: container)
|
|
// 시리 문구의 행동 이름 어휘(\(\.$action) 문구) 갱신 — 행동 추가/삭제는 다음 실행에 반영
|
|
HaruDanimShortcuts.updateAppShortcutParameters()
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
}
|
|
.modelContainer(container)
|
|
.onChange(of: scenePhase) { _, newPhase in
|
|
// 앱 사용 중 변경된 데이터를 워치에 반영 (활성/백그라운드 전환 시점)
|
|
WatchSyncManager.shared.pushSnapshot()
|
|
if newPhase == .background {
|
|
// 일기 잠금(설정 → 일기): 앱을 벗어나면 다시 잠근다
|
|
DiaryLock.shared.relock()
|
|
}
|
|
if newPhase == .active {
|
|
// 종료일이 지난 목표 판정 — 목표 탭을 열지 않아도
|
|
// 위젯·워치·모음 탭이 오래 '진행 중'으로 남지 않게 한다
|
|
Self.evaluateEndedGoals(context: container.mainContext)
|
|
// 백그라운드(워치 명령)에서 시작이 거부됐던 Live Activity 재시도 +
|
|
// 앱이 꺼진 사이 워치에서 바뀐 세션 상태를 다이나믹 아일랜드에 반영
|
|
LiveActivityManager.sync(context: container.mainContext)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 종료일이 지났고 다짐이 있는 진행 중 목표의 달성 여부를 판정해 상태를 갱신.
|
|
/// 목표 탭 진입 시(GoalListView.onAppear) 로직과 동일 — 실행/포그라운드 복귀로 시점만 추가.
|
|
/// 다짐 없는 목표는 evaluateIfEnded가 건드리지 않으므로 '확인 필요' 흐름은 그대로다.
|
|
@MainActor
|
|
private static func evaluateEndedGoals(context: ModelContext) {
|
|
let inProgress = GoalStatus.inProgress.rawValue
|
|
let goals = (try? context.fetch(FetchDescriptor<Goal>(
|
|
predicate: #Predicate { $0.statusRaw == inProgress }
|
|
))) ?? []
|
|
guard !goals.isEmpty else { return }
|
|
let math = DayMath()
|
|
var changed = false
|
|
for goal in goals {
|
|
let before = goal.status
|
|
goal.evaluateIfEnded(math: math)
|
|
if goal.status != before { changed = true }
|
|
}
|
|
if changed { DataChange.commit(context: context) }
|
|
}
|
|
}
|