diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index 69cba3b..7e56ab7 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -16,7 +16,7 @@ | 번들 ID | `com.yechan.HaruDanim` / App Group `group.com.yechan.HaruDanim` / CloudKit `iCloud.com.yechan.HaruDanim` | | 수익 모델 | 부분 유료화. 무료 한도(행동 10·목표 2·목표당 다짐 3) + 프리미엄 결제 시 해제 | | 결제 상품 | `com.yechan.HaruDanim.premium.monthly`(월 5,000원) / `.yearly`(년 50,000원) / `.lifetime`(80,000원) — 임시 가격, App Store Connect 등록은 아직 안 됨 | -| 남은 배포 작업 | App Store Connect 상품 3종 등록(구독 그룹 + 비소모성, 유료 앱 계약), 실기기 검증(펜슬 필기감·캘린더 권한), App Store 배포. **코드 측 잔여 2건**: ① 결제 화면(PremiumView)에 개인정보처리방침·이용약관 링크(심사 3.1.2 필수 — URL 준비되면 추가), ② Info.plist `ITSAppUsesNonExemptEncryption=NO` 등록 | +| 남은 배포 작업 | App Store Connect 상품 3종 등록(구독 그룹 + 비소모성, 유료 앱 계약), 실기기 검증(펜슬 필기감·캘린더 권한), App Store 배포. **코드 측 잔여 2건**: ① 결제 화면(PremiumView)에 개인정보처리방침·이용약관 링크(심사 3.1.2 필수 — URL 준비되면 추가), ② `IOS/Info.plist`(파일 있음)에 `ITSAppUsesNonExemptEncryption=NO` 추가 | --- @@ -123,6 +123,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" - 스토어 파일: App Group 컨테이너의 `HaruDanim.store` (위젯·인텐트가 같은 DB 사용). 구 샌드박스 `default.store`는 1회 이관 - **CloudKit 미러링은 메인 앱 프로세스만**. 위젯 확장(.appex)은 같은 파일을 로컬 전용으로 열음. 확장의 쓰기는 메인 앱이 원격 변경 알림으로 받아 내보냄 - iCloud 동기화 = 프리미엄 + `settings.cloudSync` 토글 (앱 재시작 시 적용). 실패 시 로컬 폴백 +- **원격 변경 가져오기(import)는 무음 푸시가 트리거** — `IOS/Info.plist`의 `UIBackgroundModes: remote-notification`(+`aps-environment` 엔타이틀먼트) 필수. 이 모드가 없으면 앱 실행/포그라운드 복귀 때만 가져와서, 다른 기기의 변경이 사용 중에는 절대 안 보이고 새로고침 버튼도 무용지물이 된다(수정된 버그). 가져오기를 코드로 강제하는 공식 API는 없음. iOS 타깃 Info.plist는 `GENERATE_INFOPLIST_FILE=YES` + `INFOPLIST_FILE=IOS/Info.plist` 병합 방식(파일에는 생성 설정에 없는 키만 넣음, IOS 동기화 그룹의 membershipException으로 리소스 복사 제외) - 컨테이너 생성 실패 시: 짧은 재시도 3회 → 스토어를 `.backup-`로 보존 후 새로 시작 (fatalError로 즉사하지 않음) - `ensureUniqueEntityIDs`: uuid 기본값 마이그레이션 중복 보정 (앱 시작 1회) @@ -135,7 +136,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" ### 5.3 변경 전파 - 앱 내 모든 mutation 끝에 **`DataChange.commit(context:)`**: save → LiveActivity sync(장시간 측정 알림 예약 동기화 포함) → `WidgetCenter.reloadAllTimelines()` + `ControlCenter.reloadAllControls()` → 워치 스냅숏 push(350ms 디바운스 — 연타 시 마지막 상태만 집계·전송). 위젯/시리 경로는 `IntentStore.performWrite`가 같은 역할(한 컨텍스트로 조회·수정·저장 원자 처리 + 저장 1회 재시도) - `CloudSyncMonitor`: NSPersistentStoreRemoteChange를 2초 디바운스로 받아 위젯·워치·LiveActivity 갱신 (화면은 건드리지 않음) -- `AppRefresh`(모음 탭 새로고침 버튼): 루트 뷰 `.id(token)` 리셋으로 모든 @Query 재조회 — 다른 기기 변경이 화면에 안 보일 때의 수동 해결책 +- `AppRefresh`(모음 탭 새로고침 버튼): 루트 뷰 `.id(token)` 리셋으로 모든 @Query 재조회 — 다른 기기 변경이 화면에 안 보일 때의 수동 해결책. **로컬 스토어에 이미 도착한 변경만 반영** — 가져오기 자체는 무음 푸시가 담당(§5.1) --- diff --git a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj index 74ce3c9..676c8aa 100644 --- a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj +++ b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj @@ -111,6 +111,13 @@ ); target = 725A9E832FFCC65000846FFB /* Haru_Danim */; }; + AA0000000000000000000013 /* Exceptions for "IOS" folder in "Haru_Danim" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 725A9E832FFCC65000846FFB /* Haru_Danim */; + }; AA0000000000000000000012 /* Exceptions for "Haru_DanimWatchWidgets" folder in "Haru_DanimWatch Watch App" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -127,6 +134,9 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 725A9E862FFCC65000846FFB /* IOS */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + AA0000000000000000000013 /* Exceptions for "IOS" folder in "Haru_Danim" target */, + ); path = IOS; sourceTree = ""; }; @@ -599,6 +609,7 @@ DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IOS/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "하루 다님"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "일기 타임테이블에 캘린더 일정을 함께 표시하기 위해 필요해요."; @@ -640,6 +651,7 @@ DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IOS/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "하루 다님"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "일기 타임테이블에 캘린더 일정을 함께 표시하기 위해 필요해요."; diff --git a/myApp/HaruDanim/IOS/Core/AppRefresh.swift b/myApp/HaruDanim/IOS/Core/AppRefresh.swift index 5b89c90..193a219 100644 --- a/myApp/HaruDanim/IOS/Core/AppRefresh.swift +++ b/myApp/HaruDanim/IOS/Core/AppRefresh.swift @@ -8,6 +8,10 @@ // 다시 조회된다 (재시작과 같은 효과, 데이터는 그대로). // - 원격 변경 자동 반영(CloudSyncMonitor)은 위젯·워치·라이브 액티비티만 갱신한다. // 화면 전체 리셋은 입력 중인 시트를 닫아버릴 수 있어 수동 버튼으로만 한다. +// ⚠️ 이 버튼은 "로컬 스토어에 이미 도착한" 변경을 화면에 반영할 뿐, CloudKit 가져오기를 +// 강제할 공식 API는 없다. 가져오기 자체는 무음 푸시(UIBackgroundModes: +// remote-notification, IOS/Info.plist)가 트리거한다 — 이 모드가 빠져 있으면 앱 +// 실행/포그라운드 복귀 때만 가져와서 새로고침을 눌러도 옛 데이터가 보인다(수정된 버그). // import Foundation diff --git a/myApp/HaruDanim/IOS/Info.plist b/myApp/HaruDanim/IOS/Info.plist new file mode 100644 index 0000000..e7fe9d7 --- /dev/null +++ b/myApp/HaruDanim/IOS/Info.plist @@ -0,0 +1,14 @@ + + + + + + UIBackgroundModes + + remote-notification + + +