feat(settings): '짧은 기록 무시'를 iCloud 키-값 저장소로 전 기기 동기화 (1.1)

- 검사가 종료 실행 기기 기준이라 기기별 값이면 교차 사용(아이패드 시작→아이폰 종료)에서
  결과가 갈리던 문제 해소 — 값 자체를 모든 기기에서 통일
- SettingsCloudSync: 시작 시 iCloud 값 채택(없으면 로컬 값 시드), 외부 변경·포그라운드 복귀 때
  pull, 설정 변경 때 push(동일 값 가드). App Group 저장은 유지(위젯·인텐트 읽기 경로 무변경),
  CloudKit 스키마·프리미엄 토글과 무관 — ubiquity-kvstore 엔타이틀먼트만 추가
- 설정: 측정 섹션을 나눠 '짧은 기록 무시' 바로 밑에 전 기기 공통 안내 푸터,
  '장시간 측정 알림'은 기기별 유지 명시. 도움말도 갱신, en/ja 번역(missing 0·stale 0, 실렌더 확인)
- whats-new-1.1 3개 언어에 항목 추가, CLAUDE.md §6.8 갱신

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVeduv1eNdXjk1ay4tSgBg
This commit is contained in:
songyc macbook 2026-07-30 02:03:29 +09:00
parent 183b0a6fa0
commit 2fb7b3c75a
8 changed files with 114 additions and 12 deletions

View File

@ -234,7 +234,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
### 6.8 설정 탭 (`SettingsView`)
- 테마(**시스템 설정 따름(기본)**/라이트/다크 — 즉시 적용, App Group 저장으로 위젯 공유. "system"은 스킴을 강제하지 않아 앱·위젯 모두 시스템 모드를 따름 — 2026-07-23 UX 감사에서 추가, 기본값도 light→system) / 언어(재시작 적용, AppleLanguages 오버라이드)
- iPhone만: 내비게이션 방식(탭바/버블) + 탭바 구성 or 버블 순서·두 번 눌러 바로 이동(토글+대상 탭)
- 모음 탭 목표 카드 선택(개수 무제한)+표시 방식 / 주 시작 요일 / 하루 시작 시간 / 짧은 기록 무시(**기기별 설정** — App Group defaults라 iCloud 동기화 안 됨. 검사는 종료를 실행한 기기의 설정 기준이고 워치 종료는 페어링 아이폰에서 실행되므로 아이폰 설정 적용 — 기기 간 시작·종료 시 값이 다르면 짧은 기록이 저장될 수 있음. 실사용 보고로 확인된 의도된 동작, 설정 푸터·도움말에 명시) / **장시간 측정 알림**(1~12시간, 켜는 순간 알림 권한 요청 — `SessionAlertManager`가 LiveActivityManager.sync 길목에서 진행 세션과 예약을 동기화, 세션 종료·설정 변경 시 예약 제거) / 대표 시간 기준(가장 먼저·나중에 시작 — 기본 latest)
- 모음 탭 목표 카드 선택(개수 무제한)+표시 방식 / 주 시작 요일 / 하루 시작 시간 / 짧은 기록 무시(**전 기기 공통 — iCloud 키-값 저장소 동기화**(1.1, `IOS/Core/SettingsCloudSync.swift` + `ubiquity-kvstore-identifier` 엔타이틀먼트): 검사는 종료를 실행한 기기의 설정 기준이라 기기별 값이면 교차 사용(아이패드 시작→아이폰 종료)에서 결과가 갈리던 실사용 보고의 해소. 값 저장은 여전히 App Group defaults고 KVS가 미러 — 시작 시 iCloud 값 채택(없으면 로컬 값 시드), 외부 변경·포그라운드 복귀 때 pull, 설정 변경 때 push(동일 값 가드로 되울림 방지). CloudKit 데이터 동기화(프리미엄 토글)와 별개라 iCloud 로그인만으로 동작, 미로그인이면 로컬 값 유지. 워치 종료는 페어링 아이폰에서 실행되므로 아이폰 설정 적용. 다른 Int 설정을 공통화하려면 syncedKeys에 추가만 하면 됨) / **장시간 측정 알림**(1~12시간, 켜는 순간 알림 권한 요청 — `SessionAlertManager`가 LiveActivityManager.sync 길목에서 진행 세션과 예약을 동기화, 세션 종료·설정 변경 시 예약 제거) / 대표 시간 기준(가장 먼저·나중에 시작 — 기본 latest)
- 데이터: **데이터 내보내기** — 행동·시간·횟수·목표·다짐을 CSV 5파일로 (ISO 8601, UTF-8 BOM, 무료 포함) / **데이터 가져오기**(1.1, 프리미엄 — `IOS/Core/CSVImport.swift`+`DataImportView`): 내보낸 CSV에서 행동·시간·횟수 기록 복원. **추가 전용**(수정·삭제 없음), 파일 선택(복수)→미리 보기→확인→저장 1회(DataChange.commit), **멱등**(기록 중복은 초 단위 절사 동일성으로 건너뜀 — 내보내기가 초 단위라 원본의 소수점 초 유사 중복까지 흡수), 파일 종류는 헤더가 아니라 **데이터 행 모양으로 판별**(UUID·ISO 날짜·time/count — 내보낸 언어와 무관), 행동 연결은 uuid 우선→이름+방식 폴백→새로 생성(csv uuid 보존, 꼬리표는 이름으로 재사용/프리셋 색 생성), '측정 중'(종료 없음) 행은 건너뜀(유령 Live Activity 방지), 목표·다짐은 비대상(다짐 CSV는 주기가 표시 문자열이라 재구성 불가 — 사용자 결정). 프리미엄 게이트 이유: CSV로 무료 한도(행동 10개) 우회 방지. 검증 `-csvImportTest`(§14)
- 프리미엄 화면(§8) / 도움말(`HelpView` — 기기별 주제 분기, DisclosureGroup. **그룹당 대표 주제 1개에 예시 스크린샷**(IOS/HelpImages/help-<이름>-<ko|en|ja>.jpg, 총 18장 ≈1.6MB) — 언어별 파일을 HelpImage 로더가 표시 언어로 선택(jpg라 UIImage(named:) 대신 URL 로드). 앱 화면·문구가 크게 바뀌면 해당 스크린샷도 재촬영할 것: 시뮬 로케일별 재시드 후 §14 인자로 촬영, 워치는 상태바 크롭) / 버전(번들 CFBundleShortVersionString 표시)

View File

@ -14,6 +14,8 @@
</array>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yechan.HaruDanim</string>

View File

@ -0,0 +1,70 @@
//
// SettingsCloudSync.swift
// Haru_Danim
//
// " " iCloud - (: )
// - (§6.8),
// .
// - CloudKit ( + cloudSync ) ·
// iCloud . .
// - : iCloud (App Group) , .
// · , push .
//
import UIKit
@MainActor
final class SettingsCloudSync {
static let shared = SettingsCloudSync()
/// (Int )
private let syncedKeys = [SettingsKeys.minSessionSeconds]
private var observers: [any NSObjectProtocol] = []
/// 1
func start() {
guard observers.isEmpty else { return }
let kvs = NSUbiquitousKeyValueStore.default
let center = NotificationCenter.default
observers.append(center.addObserver(
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: kvs, queue: nil
) { _ in
Task { @MainActor in SettingsCloudSync.shared.pull(seedIfMissing: false) }
})
observers.append(center.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil, queue: nil
) { _ in
Task { @MainActor in
NSUbiquitousKeyValueStore.default.synchronize()
SettingsCloudSync.shared.pull(seedIfMissing: false)
}
})
kvs.synchronize()
pull(seedIfMissing: true)
}
/// iCloud (App Group). 1 iCloud
/// ( ).
private func pull(seedIfMissing: Bool) {
let kvs = NSUbiquitousKeyValueStore.default
for key in syncedKeys {
if let remote = kvs.object(forKey: key) as? Int {
if AppGroup.defaults.object(forKey: key) as? Int != remote {
AppGroup.defaults.set(remote, forKey: key)
}
} else if seedIfMissing, let local = AppGroup.defaults.object(forKey: key) {
kvs.set(local, forKey: key)
}
}
}
/// ( )
func push(_ value: Int, forKey key: String) {
let kvs = NSUbiquitousKeyValueStore.default
guard kvs.object(forKey: key) as? Int != value else { return }
kvs.set(value, forKey: key)
kvs.synchronize()
}
}

View File

@ -35,6 +35,8 @@ struct Haru_DanimApp: App {
// ( shared )
PremiumManager.bootstrap(provider: StoreKitEntitlementProvider())
SettingsMigration.runIfNeeded()
// " " iCloud -
SettingsCloudSync.shared.start()
container = DataStore.shared
DataStore.ensureUniqueEntityIDs(context: container.mainContext)
// · UI : (@Model) 1

View File

@ -7026,18 +7026,18 @@
}
}
},
"설정 → 측정에서 '짧은 기록 무시'를 켜면 설정한 시간보다 짧게 측정하고 끝낸 기록은 저장하지 않아요. 행동 버튼을 실수로 눌렀다가 바로 껐을 때 의미 없는 기록이 쌓이는 것을 막아 줘요. 기준 시간은 3초부터 1분까지 고를 수 있고, 위젯·워치·시리로 종료할 때도 적용돼요. 이 설정은 기기마다 따로 저장되고 측정을 끝낸 기기의 설정이 적용되니, 아이폰과 아이패드를 함께 쓴다면 두 기기 모두 설정해 주세요. 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요.": {
"설정 → 측정에서 '짧은 기록 무시'를 켜면 설정한 시간보다 짧게 측정하고 끝낸 기록은 저장하지 않아요. 행동 버튼을 실수로 눌렀다가 바로 껐을 때 의미 없는 기록이 쌓이는 것을 막아 줘요. 기준 시간은 3초부터 1분까지 고를 수 있고, 위젯·워치·시리로 종료할 때도 적용돼요. 이 설정은 iCloud를 통해 모든 기기에서 동일하게 적용되고, 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Turn on 'Ignore short sessions' in Settings → Tracking, and sessions shorter than the time you set won't be saved. It keeps meaningless records from piling up when you tap an action button by mistake and stop it right away. You can choose from 3 seconds to 1 minute, and it also applies when you stop from widgets, Apple Watch, or Siri. This setting is saved separately on each device and the setting of the device that ends the measurement applies, so if you use both an iPhone and an iPad, set it on both. Measurements ended on Apple Watch follow the paired iPhone's setting."
"value": "Turn on 'Ignore short records' in Settings → Tracking to skip saving records that end sooner than the chosen time. It keeps meaningless entries from piling up when you tap an action button by mistake and stop right away. You can pick from 3 seconds to 1 minute, and it also applies when you stop from widgets, Apple Watch, or Siri. This setting syncs over iCloud and applies equally on every device, and a session stopped on Apple Watch follows the paired iPhone's setting."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "設定 → 計測で「短い記録を無視」をオンにすると、設定した時間より短く計測して終た記録は保存されません。アクションボタンを誤って押してすぐ止めたときに、意味のない記録がたまるのを防げます。基準時間は3秒〜1分から選べ、ウィジェット・Apple Watch・Siriから終了したときも同じように適用されます。 この設定は端末ごとに別々に保存され、計測を終了した端末の設定が適用されるため、iPhoneとiPadを併用している場合は両方で設定してください。Apple Watchで終了した計測は、ペアリングされたiPhoneの設定に従います。"
"value": "設定 → 計測で「短い記録を無視」をオンにすると、設定した時間より短く計測して終了した記録は保存されません。アクションボタンを誤って押してすぐ止めたときに、意味のない記録が溜まるのを防ぎます。基準時間は3秒から1分まで選べ、ウィジェット・Apple Watch・Siriから終了するときにも適用されます。この設定はiCloudですべてのデバイスに同じく適用され、Apple Watchで終了した計測はペアリングされたiPhoneの設定に従います。"
}
}
}
@ -7090,18 +7090,18 @@
}
}
},
"설정한 시간보다 짧게 측정하고 종료한 기록은 저장하지 않아요. 버튼을 실수로 눌렀을 때 유용해요. 장시간 측정 알림은 측정이 설정한 시간을 넘기면 알려줘요 — 끄는 걸 잊었을 때 유용해요.\n이 설정들은 기기마다 따로 저장돼요. 짧은 기록 무시는 측정을 끝낸 기기의 설정이 적용되니, 아이폰과 아이패드를 함께 쓴다면 두 기기 모두 설정해 주세요. 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요.": {
"설정한 시간보다 짧게 측정하고 종료한 기록은 저장하지 않아요. 버튼을 실수로 눌렀을 때 유용해요.\n이 설정은 iCloud를 통해 모든 기기에서 동일하게 적용돼요 — 한 기기에서 바꾸면 아이폰·아이패드·맥에 함께 반영되고, 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sessions shorter than the set time aren't saved — useful for accidental taps. The long-session alert notifies you when tracking passes the set time — useful when you forget to stop.\nThese settings are saved separately on each device. 'Ignore short sessions' uses the setting of the device that ends the measurement, so if you use both an iPhone and an iPad, set it on both. Measurements ended on Apple Watch follow the paired iPhone's setting."
"value": "Records that end sooner than the chosen time are not saved. Useful when a button is tapped by mistake.\nThis setting syncs over iCloud and applies equally on every device — change it on one device and it carries to iPhone, iPad, and Mac. A session stopped on Apple Watch follows the paired iPhone's setting."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "設定した時間より短い計測は保存されません。誤タップに便利です。長時間計測の通知は、計測が設定時間を超えるとお知らせします — 終了し忘れたときに便利です。\nこれらの設定は端末ごとに別々に保存されます。「短い記録を無視」は計測を終了した端末の設定が適用されるため、iPhoneとiPadを併用している場合は両方で設定してください。Apple Watchで終了した計測は、ペアリングされたiPhoneの設定に従います。"
"value": "設定した時間より短く計測して終了した記録は保存されません。ボタンを誤って押したときに便利です。\nこの設定はiCloudですべてのデバイスに同じく適用されます — 1台で変更するとiPhone・iPad・Macにも反映され、Apple Watchで終了した計測はペアリングされたiPhoneの設定に従います。"
}
}
}
@ -11563,6 +11563,22 @@
}
}
},
"측정이 설정한 시간을 넘기면 알려줘요 — 끄는 걸 잊었을 때 유용해요. 이 알림 설정은 기기마다 따로 저장돼요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Notifies you when a session runs past the chosen time — useful when you forget to stop. This notification setting is stored per device."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "計測が設定した時間を超えるとお知らせします — 止め忘れたときに便利です。この通知設定はデバイスごとに保存されます。"
}
}
}
},
"캘린더 일정": {
"localizations": {
"en": {

View File

@ -184,7 +184,7 @@ struct HelpView: View {
HelpTopic(
symbol: "clock.badge.xmark",
title: "짧은 기록 무시",
body: "설정 → 측정에서 '짧은 기록 무시'를 켜면 설정한 시간보다 짧게 측정하고 끝낸 기록은 저장하지 않아요. 행동 버튼을 실수로 눌렀다가 바로 껐을 때 의미 없는 기록이 쌓이는 것을 막아 줘요. 기준 시간은 3초부터 1분까지 고를 수 있고, 위젯·워치·시리로 종료할 때도 적용돼요. 이 설정은 기기마다 따로 저장되고 측정을 끝낸 기기의 설정이 적용되니, 아이폰과 아이패드를 함께 쓴다면 두 기기 모두 설정해 주세요. 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요."
body: "설정 → 측정에서 '짧은 기록 무시'를 켜면 설정한 시간보다 짧게 측정하고 끝낸 기록은 저장하지 않아요. 행동 버튼을 실수로 눌렀다가 바로 껐을 때 의미 없는 기록이 쌓이는 것을 막아 줘요. 기준 시간은 3초부터 1분까지 고를 수 있고, 위젯·워치·시리로 종료할 때도 적용돼요. 이 설정은 iCloud를 통해 모든 기기에서 동일하게 적용되고, 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요."
),
HelpTopic(
symbol: "star.fill",

View File

@ -191,17 +191,26 @@ struct SettingsView: View {
Text(option.label).tag(option.seconds)
}
}
// (§6.8)
// ( ) iCloud
.onChange(of: minSessionSeconds) { _, newValue in
SettingsCloudSync.shared.push(newValue, forKey: SettingsKeys.minSessionSeconds)
}
} header: {
Text("측정")
} footer: {
Text("설정한 시간보다 짧게 측정하고 종료한 기록은 저장하지 않아요. 버튼을 실수로 눌렀을 때 유용해요.\n이 설정은 iCloud를 통해 모든 기기에서 동일하게 적용돼요 — 한 기기에서 바꾸면 아이폰·아이패드·맥에 함께 반영되고, 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요.")
}
.id("measureSection")
Section {
Picker("장시간 측정 알림", selection: $longSessionAlertHours) {
ForEach(LongSessionAlertOption.choices, id: \.hours) { option in
Text(option.label).tag(option.hours)
}
}
} header: {
Text("측정")
} footer: {
Text("설정한 시간보다 짧게 측정하고 종료한 기록은 저장하지 않아요. 버튼을 실수로 눌렀을 때 유용해요. 장시간 측정 알림은 측정이 설정한 시간을 넘기면 알려줘요 — 끄는 걸 잊었을 때 유용해요.\n이 설정들은 기기마다 따로 저장돼요. 짧은 기록 무시는 측정을 끝낸 기기의 설정이 적용되니, 아이폰과 아이패드를 함께 쓴다면 두 기기 모두 설정해 주세요. 애플워치에서 끝낸 측정은 페어링된 아이폰의 설정을 따라요.")
Text("측정이 설정한 시간을 넘기면 알려줘요 — 끄는 걸 잊었을 때 유용해요. 이 알림 설정은 기기마다 따로 저장돼요.")
}
.id("measureSection")
Section {
Picker("대표 시간 기준", selection: $liveActivityMode) {
ForEach(LiveActivityMode.allCases) { mode in

View File

@ -4,16 +4,19 @@
• 일기를 이제 맥에서도 — 사이드바에 일기 탭이 열리고, 텍스트 입력과 함께 '그리기'를 켜면 마우스·트랙패드로도 그릴 수 있어요
• CSV 가져오기(프리미엄) — 내보냈던 CSV에서 행동과 시간·횟수 기록을 복원해요. 기존 데이터는 바꾸지 않고, 이미 있는 기록은 자동으로 건너뛰어요
• 일기 기본 표시 설정 — 캘린더 일정을 기본으로 넣을지, 어떤 목표·행동을 기본으로 숨길지 한 번만 정하면 모든 날짜에 적용돼요. 날짜별로 다르게 고르는 것도 그대로예요
• '짧은 기록 무시' 설정이 이제 iCloud로 모든 기기에서 동일하게 적용돼요 — 한 기기에서 시작하고 다른 기기에서 끝내도 같은 기준으로 판단해요
• 도움말과 안내 문구를 다듬었어요
[en]
• Diary on Mac — the Diary tab now appears in the sidebar; type as usual, or turn on 'Draw' to sketch with your mouse or trackpad
• CSV import (Premium) — restore actions and time/count records from exported CSVs. Existing data is never changed, and duplicates are skipped automatically
• Diary default display settings — decide once whether calendar events appear by default and which goals or actions stay hidden, and it applies to every date. Per-date choices still work as before
• 'Ignore short records' now syncs over iCloud and applies equally on every device — start on one device and stop on another with the same rule
• Polished help topics and descriptions
[ja]
• Macでも日記が使えるように — サイドバーに日記タブが追加され、テキスト入力に加えて「描く」をオンにするとマウス・トラックパッドで描けます
• CSV読み込みプレミアム— 書き出したCSVからアクションと時間・回数記録を復元。既存データは変更せず、重複は自動でスキップします
• 日記のデフォルト表示設定 — カレンダーの予定をデフォルトで入れるか、どの目標・アクションをデフォルトで隠すかを一度決めれば、すべての日付に適用されます。日付ごとの選択もこれまでどおり使えます
• 「短い記録を無視」の設定がiCloudですべてのデバイスに同じく適用されるように — あるデバイスで開始して別のデバイスで終了しても同じ基準で判定します
• ヘルプと説明文を改善しました