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>
108 lines
4.2 KiB
Swift
108 lines
4.2 KiB
Swift
//
|
|
// OnboardingView.swift
|
|
// Haru_Danim
|
|
//
|
|
// 첫 실행 온보딩 — 행동 → 목표·다짐 → 정리, 3장 (CLAUDE.md §6).
|
|
// 데이터가 하나도 없는 진짜 첫 사용자에게만 1회 자동 표시되고(건너뛰어도 재표시 없음),
|
|
// 설정 → 지원의 '앱 소개 다시 보기'로 언제든 다시 볼 수 있다.
|
|
// 시드·마케팅 촬영 플로우는 시드가 행동을 먼저 만들어 자동으로 건너뛰어진다.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct OnboardingView: View {
|
|
/// 한 번 보면(건너뛰기 포함) 다시 자동 표시하지 않는 플래그 (standard — 기기 로컬)
|
|
static let doneKey = "onboarding.done"
|
|
|
|
var onFinish: () -> Void
|
|
|
|
@State private var page = 0
|
|
private let lastPage = 2
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .topTrailing) {
|
|
AppTheme.background.ignoresSafeArea()
|
|
TabView(selection: $page) {
|
|
pageView(
|
|
symbol: "bolt.fill",
|
|
color: AppTheme.green,
|
|
title: "버튼 한 번으로 기록",
|
|
text: "'행동'을 만들면 탭 한 번으로 시간 측정을 시작·종료하거나 횟수를 +1 기록할 수 있어요. 여러 행동을 동시에 측정해도 돼요."
|
|
)
|
|
.tag(0)
|
|
pageView(
|
|
symbol: "flag.checkered",
|
|
color: AppTheme.green,
|
|
title: "목표와 다짐으로 꾸준하게",
|
|
text: "큰 '목표' 아래에 실천 단위인 '다짐'을 만들어요. 하루·주간·월간 진행률과 연속 달성이 자동으로 계산되고, 목표가 끝나면 달성 여부도 판정돼요."
|
|
)
|
|
.tag(1)
|
|
pageView(
|
|
symbol: "chart.xyaxis.line",
|
|
color: AppTheme.yellow,
|
|
title: "정리하고 돌아보기",
|
|
text: "꼬리표로 행동을 색깔별로 분류하고, 기록·통계 탭에서 하루를 돌아봐요. 더 자세한 사용법은 설정 탭의 도움말에 있어요."
|
|
)
|
|
.tag(2)
|
|
}
|
|
.tabViewStyle(.page(indexDisplayMode: .always))
|
|
.indexViewStyle(.page(backgroundDisplayMode: .always))
|
|
|
|
if page < lastPage {
|
|
Button("건너뛰기") { finish() }
|
|
.font(.subheadline.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
.padding(20)
|
|
}
|
|
}
|
|
.safeAreaInset(edge: .bottom) {
|
|
Button {
|
|
if page < lastPage {
|
|
withAnimation { page += 1 }
|
|
} else {
|
|
finish()
|
|
}
|
|
} label: {
|
|
Text(page < lastPage ? String(localized: "다음") : String(localized: "시작하기"))
|
|
.font(.headline)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 6)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.buttonBorderShape(.capsule)
|
|
.tint(AppTheme.green)
|
|
.padding(.horizontal, 28)
|
|
.padding(.bottom, 18)
|
|
}
|
|
}
|
|
|
|
private func finish() {
|
|
UserDefaults.standard.set(true, forKey: Self.doneKey)
|
|
onFinish()
|
|
}
|
|
|
|
private func pageView(symbol: String, color: Color,
|
|
title: LocalizedStringKey, text: LocalizedStringKey) -> some View {
|
|
VStack(spacing: 22) {
|
|
Spacer()
|
|
Image(systemName: symbol)
|
|
.font(.system(size: 64, weight: .semibold))
|
|
.foregroundStyle(color)
|
|
.frame(width: 130, height: 130)
|
|
.background(color.opacity(0.12), in: RoundedRectangle(cornerRadius: 34, style: .continuous))
|
|
Text(title)
|
|
.font(.title2.bold())
|
|
.multilineTextAlignment(.center)
|
|
Text(text)
|
|
.font(.callout)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 36)
|
|
.frame(maxWidth: 520)
|
|
Spacer()
|
|
Spacer()
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|