Merge release/3.2.3 into develop#178
Conversation
* ๐ [fix] iCloud ์๋ ๋ณต์์ผ๋ก ์ธํ ๋ก์ปฌ ๋ฐ์ดํฐ ์ด๊ธฐํ ๋ฐฉ์ง Change Token Expired(CKError 21) ๋ฐ์ ์ NSCloudKitMirroringDelegate๊ฐ ๋ก์ปฌ store๋ฅผ iOS ๋ด๋ถ ๋ก์ง์ผ๋ก purgeํ๋ฉด์ ์ฌ์ฉ์ ์์ง ๊ธฐ๋ก์ด ์ด๊ธฐํ๋๋ ๋ฒ๊ทธ๋ฅผ ์ฐจ๋จํ๋ค. ์ฐ๋ฆฌ ์ฝ๋์ ์๋ ์ญ์ ๊ฒฝ๋ก๋ฅผ ๋ชจ๋ ์ ๊ฑฐํ๊ณ , iOS๊ฐ ๋ก์ปฌ์ ์ง์ฐ๋๋ผ๋ ์ด๋ฆด ์ ์๋๋ก Documents์ ์์ ์ค๋ ์ท์ ์ ์งํ๋ค. ## ์๋ consolidation ์ ๊ฑฐ (Stage 1) - handleCloudKitEvent() Import ์๋ฃ ํ 5์ด ์ง์ฐ consolidation ์ ๊ฑฐ - SceneDelegate.setupApp() ์ฑ ์์ ์ consolidation ์ ๊ฑฐ - consolidateUserCollections/cleanupOrphanedEntities๋ ์ฌ์ฉ์๊ฐ ์ค์ ์์ "์ค๋ณต/๊ณ ์ ๋ฐ์ดํฐ ์ ๋ฆฌ" ๋ฒํผ์ ์ง์ ๋๋ ์ ๋๋ง ์คํ - ์ญ์ ์ง์ ๋ก๊ทธ๋ฅผ os_log(.error)๋ก ์น๊ฒฉํ์ฌ ํ๋ก๋์ ๊ฐ์ฌ ๊ฐ๋ฅ ## Recovery grace period ์ถ๊ฐ - performCloudKitRecovery ์คํ ์ recoveryInitiatedAt ํ์์คํฌํ ๊ธฐ๋ก - getUserCollection()์์ 10๋ถ grace window ๋ด์๋ hasEverHadUserCollection ์ฒดํฌ๋ฅผ ์ฐํํ์ฌ UC ์์ฑ ํ์ฉ โ ์ฌ์์ ํ import ์ง์ฐ๋์ด๋ ์ฑ ์ ๊น ๋ฐฉ์ง ## ์ค์ ํ๋ฉด ๊ฐ์ - "iCloud์์ ๋ณต์" 2๋จ ํ์ธ Alert๋ก ๊ฐํ + ํ๊ดด์ ๋์์์ ๋ฌธ๊ตฌ์ ๋ช ์ - "์ค๋ณต/๊ณ ์ ๋ฐ์ดํฐ ์ ๋ฆฌ" ๋ฒํผ ์ ๊ท ์ถ๊ฐ (ํ์ธ Alert ํ ์๋ ์คํ) - "๋ก์ปฌ ๋ฐฑ์ ์์ ๋ณต์" ๋ฒํผ + ๋ง์ง๋ง ๋ฐฑ์ ์๊ฐ/ํญ๋ชฉ์ ํ์ ## LocalSafetySnapshot (Stage 1.5) - Documents/local_safety_snapshot.plist์ UC ๊ทธ๋ํ๋ฅผ attribute-dict๋ก binary plist ์ง๋ ฌํํ์ฌ ์ ์ง (debounce 30s, atomic write) - ์ฑ ์์ ์ purge ์์ฌ ์ํฉ(isFreshInstall && hasEverHadUserCollection && snapshot exists) ๊ฐ์ง๋๋ฉด ๋ณต์ ์ต์ ์๋ ์ ์ - ๋ฐฑ๊ทธ๋ผ์ด๋ ์ง์ ์ง์ pending ์ค๋ ์ท ๊ฐ์ flush - ๋๋ฉ์ธ ๋ชจ๋ธ Codable ์์กด์ฑ ์์ โ NSArray/NSDictionary transformable๋ plist๊ฐ ๊ทธ๋๋ก ์ฒ๋ฆฌ ## ๋ฌธ์ - docs/features/icloud-sync.md โ ์๋ consolidation ์ ๊ฑฐ, grace period, ์๋ ๋ณต์ ํ๋ก์ฐ ๋ฐ์ - docs/plans/local-backup-split.md โ Stage 2 2-container ์ค๊ณ ๋ฐ migration/ํ ์คํธ ์ ๋ต ์ด์ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ๐ [docs] iCloud ๋ฐ์ดํฐ ์ด๊ธฐํ ๋ฒ๊ทธ ์์ ์์ฝ ๋ฌธ์ ์ถ๊ฐ ๊ธฐํยทCS ๋ด๋น์ ๋ฑ ๋น๊ฐ๋ฐ ์ธ์๋ ์ดํดํ ์ ์๋๋ก ์ด๋ฒ ์์ ์ ๋ฐฐ๊ฒฝ/ํด๊ฒฐ ๋ฐฉ์/QA ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ์ ๋ฆฌ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ๐ [fix] SafetySnapshot์ด ์ค์ ๋ก ์์ฑ๋์ง ์๋ ๋ฌธ์ ์์ ๋ก๊ทธ ๋ถ์ ๊ฒฐ๊ณผ, startObserving์ด NSManagedObjectContextDidSave๋ง ๊ด์ฐฐํ์ฌ - ์ฌ์ฉ์ ํธ์ง ์์ด ์ฑ์ ์ผ๊ณ ๋ซ์ ๊ฒฝ์ฐ ์ค๋ ์ท์ด ํ ๋ฒ๋ ์์ฑ๋์ง ์์ - CloudKit Import๋ก ๋ค์ด์จ ๋ณ๊ฒฝ์ merge ๊ฒฝ๋ก๋ผ DidSave๋ฅผ ํญ์ ํธ๋ฆฌ๊ฑฐํ์ง ์์ iOS purge๊ฐ ๋ค์ ์ธ์ ์ ๋ฐ์ํ๋ฉด Stage 1.5 ์์ ๋ง์ด ๋น์ด์๋ ์ํ๋ก ์คํจํ ์ ์์. ์์ : 1. startObserving ์ initial snapshot์ background queue์์ ์ฆ์ 1ํ ์์ฑ 2. CoreDataStorage.didFinishCloudImport / didReceiveRemoteChanges ์๋ฆผ๋ ๊ตฌ๋ ํ์ฌ CloudKit์ผ๋ก ์์ ๋ ๋ฐ์ดํฐ๋ ์ค๋ ์ท์ ๋ฐ์ 3. ๊ธฐ์กด NSManagedObjectContextDidSave ๊ด์ฐฐ์ ์ ์ง (๋ก์ปฌ ํธ์ง ๊ฒฝ๋ก) ์ด๋ก์จ ์ฑ ์คํ ์งํ "๐ SafetySnapshot written" ๋ก๊ทธ๊ฐ ๋ฐ๋์ ์ฐํ์ผ ์ ์. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ๐ [fix] SafetySnapshot serialization ์คํจ ์์ + WillReset ์ฆ์ flush ## ๋ฌธ์ 1: serialization ์คํจ (์ค๊ธฐ๊ธฐ ๋ก๊ทธ์์ ํ์ธ) ``` ๐ SafetySnapshot write failed: (ACNH_wiki.SafetySnapshotError ์ค๋ฅ 0.) ``` PropertyListSerialization์ NSString/NSNumber/NSDate/NSData/NSArray/NSDictionary๋ง ํ์ฉ. ItemEntity์ ์ผ๋ถ Transformable(variations, recipe ๋ฑ)์ NSArray/NSDictionary๋ก declared ๋์ด ์์ง๋ง ๋ด๋ถ์ ์ปค์คํ NSCoding DTO๋ฅผ ํฌํจํ๋ฏ๋ก plist ์ง๋ ฌํ ๋จ๊ณ์์ throw. ํด๊ฒฐ: PropertyListSerialization โ NSKeyedArchiver๋ก ๊ต์ฒด. - NSCoding์ ์ค์ํ๋ ๋ชจ๋ ๊ฐ์ฒด๋ฅผ ์ฒ๋ฆฌ (Core Data Transformable์ ์ด๋ฏธ NSCoding ์ ์ ). - requiringSecureCoding=false: ์ฑ์ด ์๊ธฐ ์์ ์ด ์ด ํ์ผ๋ง ๋ณตํธํํ๋ฏ๋ก ์์ ํ๋ฉฐ, ๋ชจ๋ ์ปค์คํ DTO ํด๋์ค๋ฅผ ํ์ฉ ๋ชฉ๋ก์ ๋์ดํ ํ์ ์์. - ํ์ผ์ ์ฌ๋์ด ์ฝ๊ธฐ ์ด๋ ต์ง๋ง ๋ด๋ถ ์์ ๋ง ์ฉ๋์ด๋ฏ๋ก ์์ฉ. ## ๋ฌธ์ 2: iOS purge ์ง์ ๋ง์ง๋ง ์๊ฐ flush ๋๋ฝ ์ค๊ธฐ๊ธฐ ๋ก๊ทธ์์ CKError 134301 โ WillResetSync ์๋ฆผ โ iOS๊ฐ ๋ก์ปฌ store purge ์ํ์ค ๊ด์ฐฐ๋จ. ํด๊ฒฐ: SafetySnapshotService๊ฐ "NSCloudKitMirroringDelegateWillResetSyncNotificationName" ์๋ฆผ์ ๊ตฌ๋ โ debounce ์ฐํํ์ฌ flushNow() ์ฆ์ ์คํ. WillReset ์์ ์๋ ์์ง ๋ก์ปฌ ๋ฐ์ดํฐ๊ฐ ์จ์ ํ๋ฏ๋ก, ์ด ์๊ฐ ๋์คํฌ์ ๋คํํ๋ฉด iOS purge ํ์๋ Documents/์ ๋ง์ง๋ง ์ ์ ์ํ๊ฐ ๋จ์. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * โป๏ธ [refactor] SafetySnapshot ์ค๋ณต ์ ๊ฑฐ ๋ฐ ํจ์จํ ## ์ฃผ์ ๋ณ๊ฒฝ - **DateFormatters.syncRelativeDate ์ฌ์ฌ์ฉ** (3๊ฐ ํ์ผ ์ค๋ณต ์ ๊ฑฐ): SceneDelegate, AppSettingReactor, AppSettingView์์ RelativeDateTimeFormatter๋ฅผ ๋งค๋ฒ ์ธ์คํด์คํํ๋ ๊ฒ์ ๊ธฐ์กด static ์ฑ๊ธํค์ผ๋ก ๊ต์ฒด. - **Metadata ๊ฒฝ๋ ์บ์**: readMetadata()๊ฐ ๋งค๋ฒ 3MB+ ํ์ผ์ unarchiveํ๋ ๋ฌธ์ ์์ . ์ค๋ ์ท ์ ์ฅ ์ createdAt/childCount๋ฅผ UserDefaults์ ์ฌ์ด๋์นด๋ก ๊ธฐ๋กํ๊ณ , UI๋ ์บ์์์ O(1)๋ก ์ฝ์. ํ์ผ์ด ์ธ๋ถ์์ ์ญ์ ๋๋ฉด ์บ์๋ ์ ๋ฆฌ. - **SyncResetNotification ์ฌ์ฌ์ฉ**: SafetySnapshotService๊ฐ ์ฌ์ฉํ๋ raw string "NSCloudKitMirroringDelegateWillResetSyncNotificationName"์ CoreDataStorage ๋ด๋ถ enum์ internal๋ก ๊ณต๊ฐํ ๋ค ์ฐธ์กฐํ๋๋ก ๋ณ๊ฒฝ. - **startObserving/flushNow/restore์์ container ํ๋ผ๋ฏธํฐ ์ ๊ฑฐ**: ํญ์ CoreDataStorage.shared.persistentContainer๋ฅผ ๋ฐ๋ ์ผ์ด์ค๋ฟ์ด๋ผ, ์๋น์ค ๋ด๋ถ์์ ์ง์ ์ ๊ทผ. ํธ์ถ๋ถ ๊ฐ๊ฒฐํ. - **Metadata.fileSize ํ๋ ์ญ์ **: ์ค์ ๋๊ธฐ๋ง ํ๊ณ ์ฌ์ฉ์ฒ๊ฐ ์๋ dead field. - **TOCTOU ์ ๊ฑฐ**: restore()์์ snapshotExists ์ ๊ฒ์ฌ โ Data(contentsOf:) ์ด์ค ํ์ผ ์ฒดํฌ๋ฅผ ์์ ๊ณ ์ง์ read ํ NSFileReadNoSuchFileError๋ก .noSnapshot ๋ถ๊ธฐ. - **force-unwrap ์ ๊ฑฐ**: snapshotURL์ `.first!`๋ฅผ guard let + fatalError๋ก ๊ต์ฒด (Documents ๋๋ ํ ๋ฆฌ ๋ถ์ฌ๋ ์ฌ์ค์ ๋ณต๊ตฌ ๋ถ๊ฐ ์ํ). - ์์ ํ WHAT-comment ์ ๋ฆฌ: "์๋ consolidation ์ ๊ฑฐ๋จ" ๋ฑ ์ฝ๋๊ฐ ์๋ช ํ๊ฒ ๋ณด์ฌ์ฃผ๋ ๋ด์ฉ์ ์ปค๋ฐ ํ์คํ ๋ฆฌ๋ก ์ฎ๊ธฐ๊ณ ์ฃผ์์ WHY๋ง ๋จ๊น. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.2.0 ์ดํ "๋ก์ปฌ ๋ฐ์ดํฐ๊ฐ ์ด๊ธฐํ๋์๋ค"๋ ํด๋ ์์ ์๊ฒฉ์์ ์ถ์ ํ๊ธฐ ์ํด Firebase Crashlytics + Analytics ํตํฉ ๋ํผ Log ๋์ . - Utility/Log.swift: 3-way fan-out ๋ก๊น (os_log / Crashlytics breadcrumb / Analytics) verboseยทdebugยทinfoยทwarningยทerror ๋ ๋ฒจ + eventยทclick ์ง๊ณ ์ด๋ฒคํธ + snapshot - CoreDataStorage 6๊ฐ ๊ฒฐ์ ์ง์ instrument: recovery / orphan cleanup / UC ํตํฉ / token expired / UC missing / UC ์์ฑ ์ต์ - logSyncDiagnostics + captureTelemetrySnapshot ํตํฉ (์ค๋ณต fetch ์ ๊ฑฐ) - SuppressionReasonยทUCCreationPath enum์ผ๋ก stringly-typed ํ๋ผ๋ฏธํฐ ์ ๊ฑฐ - docs/features/icloud-sync.md ์ Remote Telemetry ์น์ ์ถ๊ฐ Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
release/3.2.2 into developrelease/3.2.3 into develop
๐ WalkthroughWalkthroughThis PR implements a comprehensive local backup and recovery system to mitigate iCloud data loss. It introduces persistent local snapshots of user collections, a 10-minute recovery grace period, manual consolidation controls, enhanced centralized logging via Firebase/Crashlytics, and updated UI flows for snapshot-based restoration alongside improved iCloud recovery confirmations. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App as App Startup
participant CoreData as CoreDataStorage
participant Snapshot as SafetySnapshotService
participant UI as Alert Controller
App->>CoreData: Check hasEverHadUserCollection & isFreshInstall
App->>Snapshot: readMetadata()
Snapshot-->>App: Metadata (if snapshot exists)
alt Snapshot found & data missing
App->>UI: Show "Restore from local or wait for iCloud?"
User->>UI: Choose restore or wait
alt User chooses restore
App->>Snapshot: restore(completion:)
Snapshot->>CoreData: Load snapshot, wipe entities, apply data
Snapshot->>CoreData: Save context
Snapshot-->>App: RestoreOutcome.success(count)
App->>UI: Show "Restore complete"
App->>App: Refresh user collection
else User chooses wait
App->>CoreData: markRecoveryInitiated()
App->>CoreData: Grace period active (10 min)
end
else Snapshot not found
App->>App: Continue normal startup
end
Estimated code review effort๐ฏ 5 (Critical) | โฑ๏ธ ~110 minutes Possibly related PRs
Suggested labels
Poem
๐ฅ Pre-merge checks | โ 3 | โ 2โ Failed checks (2 warnings)
โ Passed checks (3 passed)
โ๏ธ Tip: You can configure your own custom pre-merge checks in the settings. โจ Finishing Touches๐ Generate docstrings
๐งช Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (1)
Animal-Crossing-Wiki/Projects/App/Sources/SceneDelegate.swift (1)
301-320:โ ๏ธ Potential issue | ๐ MajorStart the background task before flushing the snapshot.
flushNow()performs synchronous snapshot dump/serialization/disk I/O, but it currently runs on the main thread beforebeginBackgroundTask; a slow snapshot can block background entry and leave no extended time for the write.โฑ๏ธ Proposed ordering fix
func sceneDidEnterBackground(_ scene: UIScene) { ToastManager.shared.dismiss() - - // Stage 1.5: ๋ฐฑ๊ทธ๋ผ์ด๋ ์ง์ ์ง์ pending ์ค๋ ์ท์ ๊ฐ์ flush - SafetySnapshotService.shared.flushNow() // Extend execution time for pending CloudKit sync operations (import/export) var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid let endTask = { @@ } backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: endTask) + + // Stage 1.5: ๋ฐฑ๊ทธ๋ผ์ด๋ ์ง์ ์ง์ pending ์ค๋ ์ท์ ๊ฐ์ flush + DispatchQueue.global(qos: .utility).async { + SafetySnapshotService.shared.flushNow() + } DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { endTask() }๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Animal-Crossing-Wiki/Projects/App/Sources/SceneDelegate.swift` around lines 301 - 320, In sceneDidEnterBackground, start the background task before calling SafetySnapshotService.shared.flushNow() so the synchronous disk I/O is covered by the extended execution time: move the beginBackgroundTask(...) call (and setting of backgroundTaskID and endTask closure) above the flushNow() invocation, keep the endTask closure and the DispatchQueue.main.asyncAfter(... +30s) termination logic intact, and ensure backgroundTaskID is ended via UIApplication.shared.endBackgroundTask(backgroundTaskID) in the same endTask closure after flush completes or on the timeout.
๐งน Nitpick comments (2)
Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift (1)
162-170: Apply the SwiftLint conditional-return style.Line 164 triggers
conditional_returns_on_newline; expand theguardbody.๐งน Proposed lint fix
DispatchQueue.main.async { [weak self] in - guard let currentVC = self?.rootViewController.visibleViewController else { return } + guard let currentVC = self?.rootViewController.visibleViewController else { + return + } let title = success ? "Restore complete".localized : "Restore failed".localizedAs per coding guidelines,
**/*.swift: SwiftLint ์ค์ ์ ๋ฐ๋ผ ๋ฆฐํธ ๊ฒ์ฌ๋ฅผ ์ํํ๊ณ , ์๋ ์์ ๊ฐ๋ฅํ ๋ฌธ์ ๋swiftlint --config .swiftlint.yml --fix๋ก ์ฒ๋ฆฌ.๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift` around lines 162 - 170, The guard in showLocalRestoreResult is written as a single-line conditional which triggers SwiftLint's conditional_returns_on_newline; rewrite the guard binding to a multi-line form so the else { return } is on its own line and then place the alert creation/presentation code inside the scope that follows the guard (use the existing currentVC constant from self?.rootViewController.visibleViewController), e.g. expand the guard let currentVC = self?.rootViewController.visibleViewController into a multi-line guard and move the ensuing alert construction and currentVC.present(...) into the guarded block.Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/AppSettingReactor.swift (1)
81-134: Inconsistent[weak self]capture in chainedflatMapclosures.In
.recoverFromCloud,.consolidateManually, and.restoreLocalBackup, the outerflatMapuses[weak self]but the innerflatMap(that kicks offperformRecovery()/performConsolidation()/performLocalRestore()) capturesselfimplicitly (strong). Functionally fine because these operations are short-lived, but the mixed capture style is easy to misread as a leak, and any future refactor that moves these helpers elsewhere could regress into an actual cycle.โป๏ธ Proposed tweak for consistency
- .flatMap { confirmed -> Observable<Mutation> in - guard confirmed else { return .empty() } + .flatMap { [weak self] confirmed -> Observable<Mutation> in + guard let self, confirmed else { return .empty() } return Observable.concat( .just(.setRecoveryInProgress(true)), self.performRecovery() ) }Apply the same pattern to the
.consolidateManually(line 103) and.restoreLocalBackup(line 127) chains.๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/AppSettingReactor.swift` around lines 81 - 134, The chained flatMap closures for .consolidateManually and .restoreLocalBackup currently capture self strongly in the inner flatMap that returns Observable.concat(self.performConsolidation()/self.performLocalRestore()), creating an inconsistent capture pattern versus .recoverFromCloud; update those inner flatMap closures to use a weak self capture (e.g., flatMap { [weak self] confirmed -> Observable<Mutation> in guard let self = self, confirmed else { return .empty() } ... }) and call self.performConsolidation() / self.performLocalRestore() so the capture style matches the .recoverFromCloud chain and avoids accidental retains.
๐ค Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/SafetySnapshot/SafetySnapshotService.swift`:
- Around line 99-127: pendingWorkItem is read/written from multiple threads
causing races; funnel all accesses through the serial queue. Change
scheduleSnapshot() to perform the cancel, create the DispatchWorkItem, assign
pendingWorkItem and call queue.asyncAfter from inside queue.async (or
queue.async { ... }) so the assignment happens on queue; change flushNow() to
dispatch to queue (use queue.sync if you need synchronous flush) to cancel/clear
pendingWorkItem and then call writeSnapshotNow() on that queue; and change
stopObserving() to dispatch the pendingWorkItem cancel/clear to queue as well
(leave NotificationCenter removals on the caller thread if desired). Ensure all
references to pendingWorkItem, scheduleSnapshot(), flushNow(), stopObserving(),
and writeSnapshotNow() mutate state only on queue.
In `@Animal-Crossing-Wiki/Projects/App/Sources/SceneDelegate.swift`:
- Around line 66-74: The current implementation of
offerSafetySnapshotRestoreIfNeeded() reads metadata then waits 2 seconds before
calling presentSafetySnapshotPrompt(metadata:), but CloudKit imports can
complete during that delay; update the closure to re-check the purge conditions
before presenting: inside the DispatchQueue.main.asyncAfter block, re-evaluate
CoreDataStorage.shared.hasEverHadUserCollection and
CoreDataStorage.shared.isFreshInstall() (and optionally re-read
SafetySnapshotService.shared.readMetadata()) and only call
presentSafetySnapshotPrompt(metadata:) if those checks still pass; keep the
initial early-return checks but add this secondary validation to avoid offering
a destructive restore over newly imported data.
In `@Animal-Crossing-Wiki/Projects/App/Sources/Utility/Log.swift`:
- Around line 88-138: The Analytics calls currently send free-form text
(Param.message in emit(level:symbol:osLogType:message:sendToAnalytics:) and
Param.reason in error(name:reason:userInfo:)), which must be removed; update
emit(...) so Analytics.logEvent("log_\(level)", parameters: []) or omit
parameters entirely (do not include Param.message), and update error(...) to
call Analytics.logEvent("log_error", parameters: [Param.errorName:
truncate(name)]) only (remove Param.reason), ensuring crashlytics.record and
os_log behavior remain unchanged.
In `@docs/features/icloud-sync.md`:
- Around line 227-262: Update the stale sentence that claims
consolidateUserCollections() runs automatically on app start/after import;
replace or delete it so the doc states automatic consolidation was removed and
consolidation now runs only via
CoreDataStorage.consolidateUserCollectionsManually(completion:), and mention
that previous automatic callers (CoreDataStorage.handleCloudKitEvent() import
path and SceneDelegate.setupApp()) were removed due to the bug.
In `@docs/iCloud-data-loss-fix-summary.md`:
- Around line 175-180: The release docs state the target version is `3.2.4` but
the build config `TargetVersion.xcconfig` sets TARGET_VERSION = 3.2.3 and the PR
branch is `release/3.2.3`; reconcile them by either updating the docs section
that lists "๋์ ๋ฒ์ " to `3.2.3` to match `TARGET_VERSION` and the branch, or
change `TARGET_VERSION` in `TargetVersion.xcconfig` to `3.2.4` (and ensure the
PR/branch is updated accordingly) so the documented target, the config variable
TARGET_VERSION, and the source branch (`release/3.2.3`) are consistent.
---
Outside diff comments:
In `@Animal-Crossing-Wiki/Projects/App/Sources/SceneDelegate.swift`:
- Around line 301-320: In sceneDidEnterBackground, start the background task
before calling SafetySnapshotService.shared.flushNow() so the synchronous disk
I/O is covered by the extended execution time: move the beginBackgroundTask(...)
call (and setting of backgroundTaskID and endTask closure) above the flushNow()
invocation, keep the endTask closure and the DispatchQueue.main.asyncAfter(...
+30s) termination logic intact, and ensure backgroundTaskID is ended via
UIApplication.shared.endBackgroundTask(backgroundTaskID) in the same endTask
closure after flush completes or on the timeout.
---
Nitpick comments:
In
`@Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swift`:
- Around line 162-170: The guard in showLocalRestoreResult is written as a
single-line conditional which triggers SwiftLint's
conditional_returns_on_newline; rewrite the guard binding to a multi-line form
so the else { return } is on its own line and then place the alert
creation/presentation code inside the scope that follows the guard (use the
existing currentVC constant from
self?.rootViewController.visibleViewController), e.g. expand the guard let
currentVC = self?.rootViewController.visibleViewController into a multi-line
guard and move the ensuing alert construction and currentVC.present(...) into
the guarded block.
In
`@Animal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/AppSettingReactor.swift`:
- Around line 81-134: The chained flatMap closures for .consolidateManually and
.restoreLocalBackup currently capture self strongly in the inner flatMap that
returns
Observable.concat(self.performConsolidation()/self.performLocalRestore()),
creating an inconsistent capture pattern versus .recoverFromCloud; update those
inner flatMap closures to use a weak self capture (e.g., flatMap { [weak self]
confirmed -> Observable<Mutation> in guard let self = self, confirmed else {
return .empty() } ... }) and call self.performConsolidation() /
self.performLocalRestore() so the capture style matches the .recoverFromCloud
chain and avoids accidental retains.
๐ช Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
โน๏ธ Review info
โ๏ธ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 72d0453f-c98f-48bd-8103-2f4c36c0357a
๐ Files selected for processing (14)
Animal-Crossing-Wiki/Configurations/TargetVersion.xcconfigAnimal-Crossing-Wiki/Projects/App/Resources/en.lproj/Localizable.stringsAnimal-Crossing-Wiki/Projects/App/Resources/ko.lproj/Localizable.stringsAnimal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.swiftAnimal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/SafetySnapshot/SafetySnapshotService.swiftAnimal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/SafetySnapshot/UserCollectionSnapshot.swiftAnimal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Coordinator/DashboardCoordinator.swiftAnimal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/ViewModels/AppSettingReactor.swiftAnimal-Crossing-Wiki/Projects/App/Sources/Presentation/Dashboard/Views/AppSettingView.swiftAnimal-Crossing-Wiki/Projects/App/Sources/SceneDelegate.swiftAnimal-Crossing-Wiki/Projects/App/Sources/Utility/Log.swiftdocs/features/icloud-sync.mddocs/iCloud-data-loss-fix-summary.mddocs/plans/local-backup-split.md
| queue.async { [weak self] in | ||
| self?.writeSnapshotNow() | ||
| } | ||
| } | ||
|
|
||
| func stopObserving() { | ||
| for observer in observers { | ||
| NotificationCenter.default.removeObserver(observer) | ||
| } | ||
| observers.removeAll() | ||
| pendingWorkItem?.cancel() | ||
| pendingWorkItem = nil | ||
| } | ||
|
|
||
| private func scheduleSnapshot() { | ||
| pendingWorkItem?.cancel() | ||
| let workItem = DispatchWorkItem { [weak self] in | ||
| self?.writeSnapshotNow() | ||
| } | ||
| pendingWorkItem = workItem | ||
| queue.asyncAfter(deadline: .now() + Self.debounceSeconds, execute: workItem) | ||
| } | ||
|
|
||
| /// ๊ฐ์ ์ ์ฅ โ ์ฑ ์ข ๋ฃ ์ง์ /sync-reset ์ง์ ๋ฑ์์ flushing ์ฉ๋. | ||
| func flushNow() { | ||
| pendingWorkItem?.cancel() | ||
| pendingWorkItem = nil | ||
| writeSnapshotNow() | ||
| } |
There was a problem hiding this comment.
Data race on pendingWorkItem across threads.
pendingWorkItem is mutated from multiple threads without synchronization:
scheduleSnapshot()is invoked insideNotificationCentercallbacks registered withqueue: nil, so the closure runs on the posting thread (Core Data save โ background context thread; CloudKit events โ internal CloudKit thread). It doespendingWorkItem?.cancel()thenpendingWorkItem = workItem.flushNow()is called fromSceneDelegate(main thread) per the PR summary.stopObserving()also mutates it.- The initial
queue.async { self?.writeSnapshotNow() }at line 99 runs onqueuebut doesn't touchpendingWorkItem.
Concurrent cancel/assign from different threads is undefined behavior (torn writes, double-cancel, lost cancellation). Serialize via the existing queue (or a dedicated lock) for every access.
๐ Proposed fix โ funnel all state mutations through the serial queue
private func scheduleSnapshot() {
- pendingWorkItem?.cancel()
- let workItem = DispatchWorkItem { [weak self] in
- self?.writeSnapshotNow()
- }
- pendingWorkItem = workItem
- queue.asyncAfter(deadline: .now() + Self.debounceSeconds, execute: workItem)
+ queue.async { [weak self] in
+ guard let self else { return }
+ self.pendingWorkItem?.cancel()
+ let workItem = DispatchWorkItem { [weak self] in
+ self?.writeSnapshotNow()
+ }
+ self.pendingWorkItem = workItem
+ self.queue.asyncAfter(deadline: .now() + Self.debounceSeconds, execute: workItem)
+ }
}
func flushNow() {
- pendingWorkItem?.cancel()
- pendingWorkItem = nil
- writeSnapshotNow()
+ queue.sync {
+ self.pendingWorkItem?.cancel()
+ self.pendingWorkItem = nil
+ }
+ queue.async { [weak self] in self?.writeSnapshotNow() }
}stopObserving() should similarly dispatch the cancel onto queue.
๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/SafetySnapshot/SafetySnapshotService.swift`
around lines 99 - 127, pendingWorkItem is read/written from multiple threads
causing races; funnel all accesses through the serial queue. Change
scheduleSnapshot() to perform the cancel, create the DispatchWorkItem, assign
pendingWorkItem and call queue.asyncAfter from inside queue.async (or
queue.async { ... }) so the assignment happens on queue; change flushNow() to
dispatch to queue (use queue.sync if you need synchronous flush) to cancel/clear
pendingWorkItem and then call writeSnapshotNow() on that queue; and change
stopObserving() to dispatch the pendingWorkItem cancel/clear to queue as well
(leave NotificationCenter removals on the caller thread if desired). Ensure all
references to pendingWorkItem, scheduleSnapshot(), flushNow(), stopObserving(),
and writeSnapshotNow() mutate state only on queue.
| private func offerSafetySnapshotRestoreIfNeeded() { | ||
| guard CoreDataStorage.shared.hasEverHadUserCollection, | ||
| CoreDataStorage.shared.isFreshInstall(), | ||
| let metadata = SafetySnapshotService.shared.readMetadata() else { | ||
| return | ||
| } | ||
| DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in | ||
| self?.presentSafetySnapshotPrompt(metadata: metadata) | ||
| } |
There was a problem hiding this comment.
Re-check the purge condition before showing the restore prompt.
CloudKit import can finish during the 2-second delay, making the local store non-empty; this prompt would still offer a destructive local restore over newly imported data.
๐ก๏ธ Proposed re-validation
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
- self?.presentSafetySnapshotPrompt(metadata: metadata)
+ guard CoreDataStorage.shared.hasEverHadUserCollection,
+ CoreDataStorage.shared.isFreshInstall(),
+ let latestMetadata = SafetySnapshotService.shared.readMetadata() else {
+ return
+ }
+ self?.presentSafetySnapshotPrompt(metadata: latestMetadata)
}๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Animal-Crossing-Wiki/Projects/App/Sources/SceneDelegate.swift` around lines
66 - 74, The current implementation of offerSafetySnapshotRestoreIfNeeded()
reads metadata then waits 2 seconds before calling
presentSafetySnapshotPrompt(metadata:), but CloudKit imports can complete during
that delay; update the closure to re-check the purge conditions before
presenting: inside the DispatchQueue.main.asyncAfter block, re-evaluate
CoreDataStorage.shared.hasEverHadUserCollection and
CoreDataStorage.shared.isFreshInstall() (and optionally re-read
SafetySnapshotService.shared.readMetadata()) and only call
presentSafetySnapshotPrompt(metadata:) if those checks still pass; keep the
initial early-return checks but add this secondary validation to avoid offering
a destructive restore over newly imported data.
| private static func emit(level: String, symbol: String, osLogType: OSLogType, message: String, sendToAnalytics: Bool) { | ||
| crashlytics.log("[\(level.uppercased())] \(message)") | ||
| os_log(osLogType, log: .default, "%{public}@ %{public}@", symbol, message) | ||
| guard sendToAnalytics else { | ||
| return | ||
| } | ||
| Analytics.logEvent("log_\(level)", parameters: [Param.message: truncate(message)]) | ||
| } | ||
|
|
||
| static func verbose(_ message: String) { | ||
| #if DEBUG | ||
| emit(level: "verbose", symbol: "๐", osLogType: .debug, message: message, sendToAnalytics: true) | ||
| #else | ||
| emit(level: "verbose", symbol: "๐", osLogType: .debug, message: message, sendToAnalytics: false) | ||
| #endif | ||
| } | ||
|
|
||
| static func debug(_ message: String) { | ||
| #if DEBUG | ||
| emit(level: "debug", symbol: "๐", osLogType: .debug, message: message, sendToAnalytics: true) | ||
| #else | ||
| emit(level: "debug", symbol: "๐", osLogType: .debug, message: message, sendToAnalytics: false) | ||
| #endif | ||
| } | ||
|
|
||
| static func info(_ message: String) { | ||
| emit(level: "info", symbol: "โน๏ธ", osLogType: .info, message: message, sendToAnalytics: true) | ||
| } | ||
|
|
||
| static func warning(_ message: String) { | ||
| emit(level: "warning", symbol: "โ ๏ธ", osLogType: .error, message: message, sendToAnalytics: true) | ||
| } | ||
|
|
||
| // MARK: - Non-fatal Error | ||
|
|
||
| /// Crashlytics ๋น์น๋ช ์๋ฌ ์ ๋ก๋. ์ธ์ ์ breadcrumb + custom keys๊ฐ ํจ๊ป ์ ์ก๋๋ค. | ||
| /// ์ฌ์ฉ์ ํด๋ ์ ์ถ์ ์ ํต์ฌ ์ง์ ์ . | ||
| static func error( | ||
| name: String, | ||
| reason: String, | ||
| userInfo: [String: Any] = [:] | ||
| ) { | ||
| var info = userInfo | ||
| info[NSLocalizedDescriptionKey] = reason | ||
| let nsError = NSError(domain: "Log.\(name)", code: 0, userInfo: info) | ||
| crashlytics.record(error: nsError) | ||
| os_log(.error, log: .default, "โ๏ธ non-fatal: %{public}@ โ %{public}@", name, reason) | ||
| Analytics.logEvent("log_error", parameters: [ | ||
| Param.errorName: truncate(name), | ||
| Param.reason: truncate(reason) | ||
| ]) |
There was a problem hiding this comment.
๐งฉ Analysis chain
๐ Web query:
Firebase Analytics iOS best practices for event parameters: PII, user-generated content, and high-cardinality free-form log messages
๐ก Result:
Firebase Analytics iOS best practices for event parameters emphasize using recommended events and parameters for optimal reporting, registering custom parameters/dimensions, and avoiding issues with PII, user-generated content, and high-cardinality data. PII (Personally Identifiable Information): Do not log PII such as names, emails, phone numbers, or identifiers that can uniquely identify users in event parameters. Firebase collects device identifiers like IDFA (with consent) or app-instance ID automatically, but developers must ensure custom parameters exclude PII to comply with privacy regulations like GDPR/CCPA and avoid violations. Use anonymized or aggregated data instead. User-generated content: Avoid logging raw user-generated content (e.g., full text messages, usernames) as parameters due to PII risks and high cardinality. Truncate or hash if necessary, but prefer categorizing (e.g., "content_type": "image") over full content. Examples from docs show limited use like "name" or "full_text" only for non-sensitive recommended events. High-cardinality free-form log messages: Avoid high-cardinality parameters (>500 unique values/day, e.g., unique error messages, user IDs, paths) as they cause data sampling, "(other)" rows in reports, and hit row limits (50,000 cardinality limit). Use low-cardinality categorizations (e.g., "error_type": "network" instead of full message). For detailed logs/errors, use Firebase Crashlytics instead of Analytics events. General best practices: - Up to 25 parameters per event; names <=40 chars (alphanumeric + _, start with letter), values strings <=100 chars. - Use recommended parameters (e.g., value, currency) and register customs in console for reporting. - Prefix events logically (e.g., "home_event_select_book"). - Set user properties (up to 25) for stable attributes like "user_type", not high-cardinality or PII. - iOS-specific: Disable IDFV/IDFA collection via plist if needed; use setUserProperty for non-PII properties. Implement via Analytics.logEvent(name, parameters: [...]) in Swift.
Citations:
- 1: https://firebase.google.com/docs/analytics/ios/events
- 2: https://firebase.google.com/docs/analytics/events?platform=ios
- 3: https://firebase.google.com/docs/analytics/ios/user-properties
- 4: https://firebase.google.com/docs/analytics/events
- 5: https://groups.google.com/g/firebase-talk/c/D4_NjP4HDYk
- 6: https://stackoverflow.com/questions/63455259/effects-of-high-cardinality-google-analytics-event-label-fields
- 7: https://support.google.com/firebase/answer/6318039?hl=en
- 8: https://support.google.com/analytics/answer/11593727?hl=en
- 9: https://firebase.google.com./docs/analytics/configure-data-collection
- 10: https://firebase.google.com/docs/analytics/errors
- 11: https://support.google.com/analytics/answer/9019185?hl=en
- 12: https://www.firebase.com/terms/privacy-policy.html
- 13: https://firebase.google.com/support/privacy
- 14: https://support.google.com/analytics/answer/12226705?hl=en
- 15: https://blog.stackademic.com/mastering-firebase-analytics-in-android-track-events-with-correct-parameters-full-guide-best-a1198b321f32
๐ Script executed:
# Check the full Log.swift file to see the complete context and confirm the snippet
cat -n Projects/App/Sources/Utility/Log.swiftRepository: leeari95/ACNH-wiki
Length of output: 132
๐ Script executed:
fd -name "Log.swift" -type fRepository: leeari95/ACNH-wiki
Length of output: 292
๐ Script executed:
fd "Log.swift" --type fRepository: leeari95/ACNH-wiki
Length of output: 121
๐ Script executed:
cat -n Animal-Crossing-Wiki/Projects/App/Sources/Utility/Log.swiftRepository: leeari95/ACNH-wiki
Length of output: 9142
Don't send free-form log bodies to Firebase Analyticsโthey are high-cardinality and risk data quality issues.
Line 94 (emit()) and lines 136โ137 (error()) send arbitrary message and reason strings to Analytics. These can include dynamic Core Data/CloudKit details or user-derived content, causing high-cardinality pollution (>500 unique values/day) and exceeding quotas. Firebase best practices mandate using Crashlytics for detailed diagnostics and keeping Analytics events to low-cardinality, stable identifiers. Truncation does not solve the cardinality problem.
Remove Param.message from emit() and Param.reason from error(); keep Param.errorName only as a stable error type identifier.
๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Animal-Crossing-Wiki/Projects/App/Sources/Utility/Log.swift` around lines 88
- 138, The Analytics calls currently send free-form text (Param.message in
emit(level:symbol:osLogType:message:sendToAnalytics:) and Param.reason in
error(name:reason:userInfo:)), which must be removed; update emit(...) so
Analytics.logEvent("log_\(level)", parameters: []) or omit parameters entirely
(do not include Param.message), and update error(...) to call
Analytics.logEvent("log_error", parameters: [Param.errorName: truncate(name)])
only (remove Param.reason), ensuring crashlytics.record and os_log behavior
remain unchanged.
| ## Data Recovery (์๋ ๋ณต์) | ||
|
|
||
| ์ค์ ํ๋ฉด์์ "iCloud์์ ๋ฐ์ดํฐ ๋ณต๊ตฌ" ๊ธฐ๋ฅ ์ ๊ณต. ์์ ํ ํ ์ ๊ฑฐ ์์ . | ||
| ์ค์ ํ๋ฉด์์ "iCloud์์ ๋ณต์ (๋ก์ปฌ ๋ฐ์ดํฐ ๋ฎ์ด์)" ๊ธฐ๋ฅ ์ ๊ณต. 3.2.4๋ถํฐ ์ ์ ๊ธฐ๋ฅ์ผ๋ก ์น๊ฒฉ. | ||
| **ํ๊ดด์ ๋์** โ 2๋จ ํ์ธ Alert ํ์๋ง ์คํ๋จ. | ||
|
|
||
| **๋์ ์๋ฆฌ**: | ||
| 1. iCloud ๊ณ์ ํ์ธ โ store coordinator์์ ๊ธฐ์กด store ๋ถ๋ฆฌ | ||
| 2. SQLite ํ์ผ (.sqlite, -shm, -wal) + ckAssets ํด๋ ์ญ์ | ||
| 3. ์ฑ ์ข ๋ฃ (`exit(0)`) โ ์ฌ์์ ์ `loadPersistentStores`๊ฐ ๋น store ์์ฑ | ||
| 4. `NSPersistentCloudKitContainer`๊ฐ CloudKit์์ ์ ์ฒด ๋ฐ์ดํฐ ์๋ import | ||
| 3. `recoveryInitiatedAt` ํ์์คํฌํ ๊ธฐ๋ก (10๋ถ grace period) | ||
| 4. ์ฑ ์ข ๋ฃ (`exit(0)`) โ ์ฌ์์ ์ `loadPersistentStores`๊ฐ ๋น store ์์ฑ | ||
| 5. `NSPersistentCloudKitContainer`๊ฐ CloudKit์์ ์ ์ฒด ๋ฐ์ดํฐ ์๋ import | ||
|
|
||
| **Recovery Grace Period (10๋ถ)**: | ||
| - ์ฌ์์ ํ CloudKit import๊ฐ ์ง์ฐ๋๊ฑฐ๋ ์คํจํด๋ ์ฑ์ด ์ฌ์ฉ ๊ฐ๋ฅ ์ํ๊ฐ ๋๋๋ก ๋ณด์ฅ | ||
| - `getUserCollection()`์์ `hasEverHadUserCollection == true`์ด๋๋ผ๋ grace ๊ธฐ๊ฐ ๋ด์๋ UC ์ ๊ท ์์ฑ ํ์ฉ | ||
| - 10๋ถ ๊ฒฝ๊ณผ ๋๋ ์ ์ import ์๋ฃ ์ ํ๋๊ทธ ์๋ ์ ๋ฆฌ | ||
|
|
||
| **๊ด๋ จ ํ์ผ** (๋ชจ๋ `// TEMPORARY: Recovery` ์ฃผ์): | ||
| - `CoreDataStorage.performCloudKitRecovery()`, `RecoveryError` | ||
| - `AppSettingReactor` โ `.recoverFromCloud` Action, `.setRecoveryInProgress` Mutation | ||
| **๊ด๋ จ ํ์ผ**: | ||
| - `CoreDataStorage.performCloudKitRecovery()`, `RecoveryError`, `markRecoveryInitiated`, `isWithinRecoveryGracePeriod` | ||
| - `AppSettingReactor` โ `.recoverFromCloud` Action (2๋จ ํ์ธ), `.setRecoveryInProgress` Mutation | ||
| - `AppSettingView` โ ๋ณต๊ตฌ ๋ฒํผ + ActivityIndicator | ||
| - `DashboardCoordinator.showRecoveryResultAlert()` | ||
| - `Localizable.strings` (ko/en) โ ๋ณต๊ตฌ ๊ด๋ จ ๋ฌธ์์ด 6๊ฐ | ||
| - `Localizable.strings` (ko/en) โ ๋ณต๊ตฌ ๊ด๋ จ ๋ฌธ์์ด | ||
|
|
||
| ## Manual Consolidation (์ค๋ณต/๊ณ ์ ๋ฐ์ดํฐ ์ ๋ฆฌ) | ||
|
|
||
| **3.2.4๋ถํฐ ์๋ consolidation ์ ๊ฑฐ๋จ** โ ๋ก์ปฌ ๋ฐ์ดํฐ๊ฐ ์๋์น ์๊ฒ ์ญ์ ๋๋ ๋ฒ๊ทธ๋ก ์ธํด, | ||
| ์ฌ์ฉ์๊ฐ ์ค์ ์์ "์ค๋ณต/๊ณ ์ ๋ฐ์ดํฐ ์ ๋ฆฌ" ๋ฒํผ์ ์ง์ ๋๋ ์ ๋๋ง ์คํ. | ||
|
|
||
| **์ ๊ฑฐ๋ ์๋ ํธ์ถ๋ถ**: | ||
| - ~~`CoreDataStorage.handleCloudKitEvent()` Import ์๋ฃ ํ 5์ด ์ง์ฐ~~ | ||
| - ~~`SceneDelegate.setupApp()` ์ฑ ์์ ์~~ | ||
|
|
||
| **์๋ ํธ์ถ**: | ||
| - `CoreDataStorage.consolidateUserCollectionsManually(completion:)` โ ์ค์ ๋ฒํผ์์๋ง ํธ์ถ | ||
| - `consolidateUserCollections()` (๊ธฐ์กด ํจ์)๋ ์ ์งํ๋ ์๋ ํธ์ถ์ฒ ์์ |
There was a problem hiding this comment.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Verify whether exit(0) / kill are invoked as part of the recovery flow
rg -nP --type=swift -C3 '\bexit\s*\(\s*0\s*\)'
rg -nP --type=swift -C3 'showRecoveryResultAlert\s*\('Repository: leeari95/ACNH-wiki
Length of output: 3615
๐ Script executed:
#!/bin/bash
# View the context around line 190 to verify the stale consolidation description
sed -n '185,195p' docs/features/icloud-sync.mdRepository: leeari95/ACNH-wiki
Length of output: 704
๐ Script executed:
#!/bin/bash
# Check CoreDataStorage.swift around lines 410-411 for consolidation removal
sed -n '405,420p' Animal-Crossing-Wiki/Projects/App/Sources/CoreDataStorage/CoreDataStorage.swiftRepository: leeari95/ACNH-wiki
Length of output: 640
Remove stale automatic consolidation description at line 190.
The line still reads: "consolidateUserCollections() โ ์ฑ ์์/Import ์๋ฃ ์ ์๋ ์คํ (5์ด ์ง์ฐ, DispatchWorkItem์ผ๋ก ์ค๋ณต ๋ฐฉ์ง)". This contradicts the new 3.2.4 note and the Manual Consolidation section (lines 253โ262) which correctly state that automatic calls were removed. CoreDataStorage.swift confirms the removal with the comment: "Import ํ ์๋ consolidation/orphan cleanup์ด ๋ก์ปฌ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ ๋ฒ๊ทธ๋ก ์ ๊ฑฐ๋จ". Update or delete this line to reflect manual-only invocation.
๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/features/icloud-sync.md` around lines 227 - 262, Update the stale
sentence that claims consolidateUserCollections() runs automatically on app
start/after import; replace or delete it so the doc states automatic
consolidation was removed and consolidation now runs only via
CoreDataStorage.consolidateUserCollectionsManually(completion:), and mention
that previous automatic callers (CoreDataStorage.handleCloudKitEvent() import
path and SceneDelegate.setupApp()) were removed due to the bug.
| ## ๐ฆ ์ด๋ฒ ๋ฆด๋ฆฌ์ค ์ ๋ณด | ||
|
|
||
| - **๋ธ๋์น**: `fix/icloud-auto-restore-data-loss` | ||
| - **๊ธฐ๋ฐ ๋ฒ์ **: `release/3.2.3` | ||
| - **๋์ ๋ฒ์ **: `3.2.4` ์์ | ||
| - **์ปค๋ฐ**: `๐ [fix] iCloud ์๋ ๋ณต์์ผ๋ก ์ธํ ๋ก์ปฌ ๋ฐ์ดํฐ ์ด๊ธฐํ ๋ฐฉ์ง` |
There was a problem hiding this comment.
Align the documented release version with the actual target config.
This section says the target version is 3.2.4, but TargetVersion.xcconfig now sets TARGET_VERSION = 3.2.3 and the PR source branch is release/3.2.3. Please update either the docs or the version config before release.
๐ค Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/iCloud-data-loss-fix-summary.md` around lines 175 - 180, The release
docs state the target version is `3.2.4` but the build config
`TargetVersion.xcconfig` sets TARGET_VERSION = 3.2.3 and the PR branch is
`release/3.2.3`; reconcile them by either updating the docs section that lists
"๋์ ๋ฒ์ " to `3.2.3` to match `TARGET_VERSION` and the branch, or change
`TARGET_VERSION` in `TargetVersion.xcconfig` to `3.2.4` (and ensure the
PR/branch is updated accordingly) so the documented target, the config variable
TARGET_VERSION, and the source branch (`release/3.2.3`) are consistent.
Summary by CodeRabbit
Release Notes: Version 3.2.3
New Features
Improvements