Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
10 commits
Select commit Hold shift + click to select a range
c7e7811
Fix iCloud sync reset guard
Jun 3, 2026
1b1fb73
Harden iCloud reset regression coverage
Jun 3, 2026
6710d5b
Stabilize iCloud reset regression wait
Jun 3, 2026
484a13f
โ™ป๏ธ [refactor] PR #179 ๋ฆฌ๋ทฐ ์ฝ”๋ฉ˜ํŠธ ๋ฐ˜์˜ โ€” ํ…Œ์ŠคํŠธ seam ๊ฒฉ๋ฆฌยท์ง„๋‹จยท์ปจ๋ฒค์…˜ ๋ณด๊ฐ•
claude Jun 4, 2026
ca2391e
Merge pull request #180 from leeari95/claude/stoic-dirac-DCSE9
geeksbaek Jun 10, 2026
621bbf0
๐Ÿ› [fix] ๋™๊ธฐํ™” ์ž”์กด ๋ฒ„๊ทธ ์ˆ˜์ • โ€” orphan cleanup grace ๊ฐ€๋“œยทrecovery ํ”Œ๋ž˜๊ทธ ์ •๋ฆฌยท๋ณดํ˜ธ ๋ชจ๋“œ ์•ˆ๋‚ด
Jun 10, 2026
4cfa026
๐ŸŽจ [style] SwiftLint autocorrect ์ž๋™ ์ˆ˜์ • ๋ฐ˜์˜ (Catalog)
Jun 10, 2026
ede23f6
๐Ÿ› [fix] ์ „ ๊ธฐ๋Šฅ ์˜์—ญ ๋ฒ„๊ทธ ์ผ๊ด„ ์ˆ˜์ • โ€” ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ๋ˆ„๋ฝยท๋งคํ•‘ ์˜ค๋ฅ˜ยทํฌ๋ž˜์‹œยทํ”Œ๋ ˆ์ด์–ด ์˜ต์ €๋ฒ„
Jun 10, 2026
5399107
๐Ÿ“ [docs] ๋ฒ„๊ทธ ์ˆ˜์ • ๊ด€๋ จ ๋ฌธ์„œ ๋™๊ธฐํ™” โ€” music-playerยทdashboardยทicloud-sync
Jun 10, 2026
ca5475d
Merge branch 'release/3.2.4' into claude/fix-cross-feature-bugs
leeari95 Jul 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Find the villagers you have visited and tap the home icon on the villager's page
"Restore complete" = "Restore complete";
"Restored %d items" = "Restored %d items.";
"Restore failed" = "Restore failed";
"Unknown error" = "Unknown error";
"No local backup found" = "No local backup found.";
"Local backup info" = "Last local backup";
"Local backup info format" = "%@ ยท %d items";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"Restore complete" = "๋ณต์› ์™„๋ฃŒ";
"Restored %d items" = "%d๊ฐœ ํ•ญ๋ชฉ์„ ๋ณต์›ํ–ˆ์Šต๋‹ˆ๋‹ค.";
"Restore failed" = "๋ณต์› ์‹คํŒจ";
"Unknown error" = "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜";
"No local backup found" = "์ €์žฅ๋œ ๋กœ์ปฌ ๋ฐฑ์—…์ด ์—†์Šต๋‹ˆ๋‹ค.";
"Local backup info" = "๋งˆ์ง€๋ง‰ ๋กœ์ปฌ ๋ฐฑ์—…";
"Local backup info format" = "%@ ยท ํ•ญ๋ชฉ %d๊ฐœ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final class AppCoordinator: Coordinator {

let animalsCoordinator = AnimalsCoordinator()
animalsCoordinator.start()
animalsCoordinator.setUpParent(to: self)
addViewController(animalsCoordinator.rootViewController, title: "animals".localized, icon: "icon-book-tabbar")
childCoordinators.append(animalsCoordinator)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ final class CoreDataDailyTaskStorage: DailyTaskStorage {
let itemEntities = object.dailyTasks?.allObjects as? [DailyTaskEntity] ?? []
if let index = itemEntities.firstIndex(where: { $0.id == task.id }) {
var progressList = (itemEntities[index].progressList as? [Bool]) ?? []

// CloudKit ๋ถ€๋ถ„ import ํ›„ ์ €์žฅ๋œ progressList๊ฐ€ ์งง์„ ์ˆ˜ ์žˆ์Œ โ€” ๊ธธ์ด๋ฅผ ๋ณด์ •ํ•ด
// index out of range ํฌ๋ž˜์‹œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์†์ƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๊ฐ€ ์น˜์œ ํ•œ๋‹ค.
if progressList.count <= progressIndex {
let padding = progressIndex + 1 - progressList.count
progressList.append(contentsOf: Array(repeating: false, count: padding))
}
progressList[progressIndex] = !progressList[progressIndex]
itemEntities[index].progressList = progressList as NSArray
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,22 @@ extension DailyTaskEntity {
}

func toDomain() -> DailyTask {
let amount = Int(self.amount)

// CloudKit ๋ถ€๋ถ„ import/sync reset ํ›„ progressList๊ฐ€ nil์ด๊ฑฐ๋‚˜ amount๋ณด๋‹ค ์งง์„ ์ˆ˜ ์žˆ์Œ.
// UI๋Š” amount ๊ธฐ์ค€์œผ๋กœ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ๊ธธ์ด๋ฅผ amount์— ๋งž์ถฐ ์ •๊ทœํ™”ํ•ด
// index out of range ํฌ๋ž˜์‹œ๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค.
var progressList = (self.progressList as? [Bool]) ?? []
if progressList.count < amount {
progressList.append(contentsOf: Array(repeating: false, count: amount - progressList.count))
}

return DailyTask(
id: self.id ?? UUID(),
name: self.name ?? "",
icon: self.icon ?? "",
progressList: (self.progressList as? [Bool]) ?? [],
amount: Int(self.amount),
progressList: progressList,
amount: amount,
createdDate: self.date ?? Date()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ final class SafetySnapshotService {
private static let debounceSeconds: TimeInterval = 30

private let queue = DispatchQueue(label: "app.safety.snapshot", qos: .utility)
private var pendingWorkItem: DispatchWorkItem?

/// CoreData ์ €์žฅ ์•Œ๋ฆผ(์ž„์˜ ์Šค๋ ˆ๋“œ)ยทmain ์Šค๋ ˆ๋“œยทsync-reset ์•Œ๋ฆผ์ด ๋™์‹œ์— ์ ‘๊ทผํ•˜๋ฏ€๋กœ lock์œผ๋กœ ๋ณดํ˜ธ
private let _pendingWorkItem = OSAllocatedUnfairLock<DispatchWorkItem?>(initialState: nil)
private var observers: [NSObjectProtocol] = []
private let containerProvider: () -> NSPersistentContainer
private let snapshotDirectoryProvider: () -> URL?
Expand Down Expand Up @@ -131,24 +133,33 @@ final class SafetySnapshotService {
NotificationCenter.default.removeObserver(observer)
}
observers.removeAll()
pendingWorkItem?.cancel()
pendingWorkItem = nil
_pendingWorkItem.withLock { item in
item?.cancel()
item = nil
}
}

private func scheduleSnapshot() {
pendingWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
self?.writeSnapshotNow()
}
pendingWorkItem = workItem
_pendingWorkItem.withLock { item in
item?.cancel()
item = workItem
}
queue.asyncAfter(deadline: .now() + Self.debounceSeconds, execute: workItem)
}

/// ๊ฐ•์ œ ์ €์žฅ โ€” ์•ฑ ์ข…๋ฃŒ ์ง์ „/sync-reset ์ง์ „ ๋“ฑ์—์„œ flushing ์šฉ๋„.
/// debounce ์ค‘์ธ ์“ฐ๊ธฐ์™€ ํŒŒ์ผ I/O๊ฐ€ ๊ฒน์น˜์ง€ ์•Š๋„๋ก queue์—์„œ ๋™๊ธฐ ์‹คํ–‰ํ•œ๋‹ค.
func flushNow() {
pendingWorkItem?.cancel()
pendingWorkItem = nil
writeSnapshotNow()
_pendingWorkItem.withLock { item in
item?.cancel()
item = nil
}
queue.sync { [weak self] in
self?.writeSnapshotNow()
}
}

private func writeSnapshotNow() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extension NPCLikeEntity {
iconImage: self.iconImage ?? "",
photoImage: self.photoImage ?? "",
gender: Gender(rawValue: self.gender ?? "") ?? .male,
genderAsia: Gender(rawValue: self.gender ?? "") ?? .male,
genderAsia: Gender(rawValue: self.genderAsia ?? "") ?? .male,
species: self.species ?? "",
birthday: self.birthday ?? "",
appearanceLocation: appearanceLocation ?? [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ struct DailyTask {
let createdDate: Date

mutating func toggleCompleted(_ index: Int) {
// ๋™๊ธฐํ™”๋กœ progressList๊ฐ€ amount๋ณด๋‹ค ์งง์•„์งˆ ์ˆ˜ ์žˆ์Œ โ€” ๊ธธ์ด๋ฅผ ๋ณด์ •ํ•ด ํฌ๋ž˜์‹œ ๋ฐฉ์ง€
if progressList.count <= index {
progressList.append(contentsOf: Array(repeating: false, count: index + 1 - progressList.count))
}
self.progressList[index].toggle()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ final class NPCCellReactor: Reactor {
HapticManager.shared.impact(style: .medium)
Items.shared.updateNPCLike(npc)
likeStorage.update(npc)
newState.isLiked = !(newState.isLiked ?? false)
}
return newState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ final class VillagersCellReactor: Reactor {
HapticManager.shared.impact(style: .medium)
Items.shared.updateVillagerHouse(villager)
houseStorage.update(villager)
newState.isResident = !(newState.isResident ?? false)

case .updateLike:
HapticManager.shared.impact(style: .medium)
Items.shared.updateVillagerLike(villager)
likeStorage.update(villager)
newState.isLiked = !(newState.isLiked ?? false)
}
return newState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ extension Items {
func reset() {
villagersLike.accept([])
villagersHouse.accept([])
npcLike.accept([])
let currentItems = userItems.value.map { $0.key }
var resetItem = userItems.value
currentItems.forEach { category in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ final class MusicPlayerManager {
static let shared = MusicPlayerManager()

private let disposeBag = DisposeBag()
private var backgroundDisposeBag = DisposeBag()
private var player: AVPlayer?
private var timer: Timer?
private var endPlaybackObserver: NSObjectProtocol?

private let isPlaying = BehaviorRelay<Bool?>(value: nil)
private let currentSong = BehaviorRelay<Item?>(value: nil)
Expand Down Expand Up @@ -54,6 +56,9 @@ final class MusicPlayerManager {
.compactMap { URL(string: $0) }
.subscribe(with: self, onNext: { owner, musicURL in
owner.player = AVPlayer(url: musicURL)
// ๊ณก์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค(player ์žฌ์ƒ์„ฑ) ์ข…๋ฃŒ ์•Œ๋ฆผ ์˜ต์ €๋ฒ„๋ฅผ ์žฌ๋“ฑ๋กํ•ด
// ์ž๋™ ๋‹ค์Œ ๊ณก/๋ฐ˜๋ณต ์žฌ์ƒ์ด ๋Š๊ธฐ์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•œ๋‹ค.
owner.setUpNotification()
}).disposed(by: disposeBag)

currentSong
Expand Down Expand Up @@ -144,16 +149,25 @@ final class MusicPlayerManager {
}

private func setUpNotification() {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)

NotificationCenter.default.addObserver(
// block ๊ธฐ๋ฐ˜ ์˜ต์ €๋ฒ„๋Š” removeObserver(self,...)๋กœ ์ œ๊ฑฐ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ํ† ํฐ์„ ๋ณด๊ด€ํ•ด ์ œ๊ฑฐํ•œ๋‹ค.
// object๋ฅผ ํŠน์ • AVPlayerItem์— ๋ฐ”์ธ๋”ฉํ•˜๋ฉด ๊ณก ์ „ํ™˜(changeSong) ํ›„ ์•Œ๋ฆผ์„ ๋ชป ๋ฐ›์œผ๋ฏ€๋กœ
// object: nil๋กœ ๋“ฑ๋กํ•˜๊ณ  ํ•ธ๋“ค๋Ÿฌ์—์„œ ํ˜„์žฌ ์žฌ์ƒ item์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
if let endPlaybackObserver {
NotificationCenter.default.removeObserver(endPlaybackObserver)
}

endPlaybackObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: player?.currentItem,
object: nil,
queue: .main
) { [weak self] _ in
) { [weak self] notification in
guard let owner = self else {
return
}
guard let endedItem = notification.object as? AVPlayerItem,
endedItem === owner.player?.currentItem else {
return
}
owner.playerProgress.accept(0)
owner.elapsedTime.accept("0:00")
owner.isPlaying.accept(false)
Expand All @@ -180,6 +194,9 @@ final class MusicPlayerManager {
playingInfo[MPMediaItemPropertyAlbumTitle] = "K.K. Slider"
playingInfo[MPMediaItemPropertyTitle] = currentSong.translations.localizedName()

// ๊ณก ์ „ํ™˜ ์‹œ ์ด์ „ ๊ณก์˜ ์•„ํŠธ์›Œํฌ ๋‹ค์šด๋กœ๋“œ๋ฅผ ์ทจ์†Œํ•ด, ๋Šฆ๊ฒŒ ๋„์ฐฉํ•œ ์ด๋ฏธ์ง€๊ฐ€
// ์ž ๊ธˆํ™”๋ฉด now playing ์ •๋ณด๋ฅผ ๋ฎ์–ด์“ฐ๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.
backgroundDisposeBag = DisposeBag()
UIImage.downloadImage(urlString: currentSong.image ?? "")
.compactMap { $0 }
.subscribe(onNext: { image in
Expand All @@ -189,7 +206,7 @@ final class MusicPlayerManager {
}
)
MPNowPlayingInfoCenter.default().nowPlayingInfo = playingInfo
}).disposed(by: disposeBag)
}).disposed(by: backgroundDisposeBag)
}
}

Expand Down Expand Up @@ -295,7 +312,6 @@ extension MusicPlayerManager {
close()
currentSong.accept(item)
isPlaying.accept(true)
setUpNotification()
}

func close() {
Expand All @@ -309,9 +325,12 @@ extension MusicPlayerManager {
elapsedTime.accept("0:00")
durationTime.accept("0:00")
playerProgress.accept(0)

NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)


if let endPlaybackObserver {
NotificationCenter.default.removeObserver(endPlaybackObserver)
self.endPlaybackObserver = nil
}

try? AVAudioSession.sharedInstance().setActive(false, options: [])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,56 @@ final class CoreDataStorageICloudResetTests: XCTestCase {
XCTAssertTrue(storage.isWithinRecoveryGracePeriod)
}

func testCorruptedProgressListNormalizedOnToDomain() throws {
let storage = try makeStorage()
let context = storage.persistentContainer.viewContext

let userCollection = try insertUserCollection(in: context)
let taskEntity = try insertDailyTask(in: context, linkedTo: userCollection)
taskEntity.amount = 6
taskEntity.progressList = [] as NSArray // CloudKit ๋ถ€๋ถ„ import๋กœ ์†์ƒ๋œ ์ƒํƒœ
try context.save()

let domain = taskEntity.toDomain()

XCTAssertEqual(domain.progressList.count, 6)
XCTAssertEqual(domain.progressList, Array(repeating: false, count: 6))
}

func testToggleCompletedOnCorruptedProgressListDoesNotCrashAndHeals() throws {
let storage = try makeStorage()
let context = storage.persistentContainer.viewContext

let userCollection = try insertUserCollection(in: context)
let taskEntity = try insertDailyTask(in: context, linkedTo: userCollection)
taskEntity.amount = 6
taskEntity.progressList = [] as NSArray
try context.save()

var domainTask = taskEntity.toDomain()

// ๋ชจ๋ธ toggle โ€” ์†์ƒ ๊ธธ์ด์—์„œ๋„ ํฌ๋ž˜์‹œ ์—†์ด ๋ณด์ • ํ›„ ํ† ๊ธ€
domainTask.toggleCompleted(5)
XCTAssertEqual(domainTask.progressList.count, 6)
XCTAssertTrue(domainTask.progressList[5])

// ์ €์žฅ์†Œ toggle โ€” background context์—์„œ ๋ณด์ • ํ›„ ์ €์žฅ๋˜๋Š”์ง€ ํด๋ง์œผ๋กœ ํ™•์ธ
let taskStorage = CoreDataDailyTaskStorage(coreDataStorage: storage)
taskStorage.toggleCompleted(domainTask, progressIndex: 5)

let deadline = Date().addingTimeInterval(5)
var healed = false
while !healed && Date() < deadline {
context.refreshAllObjects()
if let stored = taskEntity.progressList as? [Bool], stored.count == 6, stored[5] {
healed = true
} else {
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
}
XCTAssertTrue(healed, "storage toggleCompleted should heal corrupted progressList without crashing")
}

func testSyncProtectionStateDetectsSuppressedEmptyCollection() throws {
let storage = try makeStorage()
let context = storage.persistentContainer.viewContext
Expand Down
4 changes: 4 additions & 0 deletions docs/features/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ viewController.setUpViewModels(

## Special Patterns

- **DailyTask progressList ์ •๊ทœํ™”**: CloudKit ๋ถ€๋ถ„ import/sync reset ํ›„ `DailyTaskEntity.progressList`๊ฐ€
nil์ด๊ฑฐ๋‚˜ `amount`๋ณด๋‹ค ์งง์„ ์ˆ˜ ์žˆ๋‹ค. UI๋Š” `amount` ๊ธฐ์ค€์œผ๋กœ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์—
`toDomain()`์—์„œ ๊ธธ์ด๋ฅผ `amount`๋กœ ๋ณด์ •ํ•˜๊ณ , `toggleCompleted`(๋ชจ๋ธยทstorage ์–‘์ชฝ)๋„
๋ณด์ • ํ›„ ํ† ๊ธ€ํ•œ๋‹ค โ€” index out of range ํฌ๋ž˜์‹œ ๋ฐฉ์ง€ + ์†์ƒ ๋ฐ์ดํ„ฐ ์ž๊ฐ€ ์น˜์œ 
- **CustomTaskViewControllerDelegate**: DashboardCoordinator๊ฐ€ IconChooser โ†’ CustomTaskVC ๊ฐ„ ์•„์ด์ฝ˜ ์„ ํƒ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•˜๋Š” delegate
- **showAlert()**: DashboardCoordinator์˜ `showAlert(title:message:) -> Observable<Bool>` ๋ฉ”์„œ๋“œ๋กœ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ
- **showRecoveryResultAlert()**: (TEMPORARY) iCloud ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ ๊ฒฐ๊ณผ ์•Œ๋ฆผ. ์„ฑ๊ณต ์‹œ ์•ฑ ์ข…๋ฃŒ ์œ ๋„
Expand Down
3 changes: 3 additions & 0 deletions docs/features/icloud-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ CloudKit import, remote change, sync reset ์ง์ „์—๋Š” ์ตœ์‹  ๋กœ์ปฌ ์ƒํƒœ๋ฅผ
- ์Šค๋ƒ…์ƒท ํŒŒ์ผ์€ ์ฒซ ์ž ๊ธˆ ํ•ด์ œ ํ›„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ CloudKit flush์—์„œ๋„ ๊ฐฑ์‹ ๋  ์ˆ˜ ์žˆ๋„๋ก `completeUntilFirstUserAuthentication` ๋ณดํ˜ธ ๋“ฑ๊ธ‰์œผ๋กœ ์ €์žฅํ•œ๋‹ค.
- ํŒŒ์ผ์€ ๊ธฐ๊ธฐ/iCloud ๋ฐฑ์—…์—์„œ ์ œ์™ธํ•˜์—ฌ Core Data ์›๋ณธ๊ณผ ๋ณ„๋„๋กœ ์žฅ๊ธฐ ๋ณด๊ด€๋˜์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค.
- ๋ณต์›์€ `wipeExistingCollection โ†’ snapshot.apply โ†’ context.save()`๋ฅผ ๋‹จ์ผ context rollback ๊ฒฝ๊ณ„์— ๋ฌถ๋Š”๋‹ค. ์ค‘๊ฐ„ ์‹คํŒจ ์‹œ ๊ธฐ์กด ๋กœ์ปฌ ์ปฌ๋ ‰์…˜ ์‚ญ์ œ๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค.
- debounce์šฉ `pendingWorkItem`์€ CoreData ์ €์žฅ ์•Œ๋ฆผ(์ž„์˜ ์Šค๋ ˆ๋“œ)ยทmain ์Šค๋ ˆ๋“œยทsync-reset ์•Œ๋ฆผ์ด
๋™์‹œ์— ์ ‘๊ทผํ•˜๋ฏ€๋กœ `OSAllocatedUnfairLock`์œผ๋กœ ๋ณดํ˜ธํ•˜๊ณ , `flushNow()`๋Š” debounce ์“ฐ๊ธฐ์™€
ํŒŒ์ผ I/O๊ฐ€ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ์ „์šฉ queue์—์„œ ๋™๊ธฐ ์‹คํ–‰ํ•œ๋‹ค.

## Manual Consolidation (์ค‘๋ณต/๊ณ ์•„ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ)

Expand Down
6 changes: 6 additions & 0 deletions docs/features/music-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ topAnchor constraint ์กฐ์ ˆ + spring animation์œผ๋กœ minimize/maximize ์ „ํ™˜.
- `MPRemoteCommandCenter` ์—ฐ๋™ (์ž ๊ธˆํ™”๋ฉด ์ปจํŠธ๋กค)
- ์žฌ์ƒ ๋ชจ๋“œ: shuffle, fullRepeat, oneSongRepeat

**๊ณก ์ข…๋ฃŒ ์•Œ๋ฆผ (`AVPlayerItemDidPlayToEndTime`) ์ฒ˜๋ฆฌ ๊ทœ์น™**:
- block ๊ธฐ๋ฐ˜ ์˜ต์ €๋ฒ„๋Š” `removeObserver(self,...)`๋กœ ์ œ๊ฑฐ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ **ํ† ํฐ**(`endPlaybackObserver`)์„ ๋ณด๊ด€ํ•ด ์ œ๊ฑฐํ•œ๋‹ค (๋ˆ„์  ๋“ฑ๋ก ์‹œ ์ž๋™ ๋‹ค์Œ ๊ณก์ด ์—ฌ๋Ÿฌ ๋ฒˆ ์‹คํ–‰๋˜๋Š” ๋ฒ„๊ทธ ๋ฐฉ์ง€)
- `object: nil`๋กœ ๋“ฑ๋กํ•˜๊ณ  ํ•ธ๋“ค๋Ÿฌ์—์„œ `notification.object === player?.currentItem`์„ ํ™•์ธํ•œ๋‹ค โ€” ํŠน์ • `AVPlayerItem`์— ๋ฐ”์ธ๋”ฉํ•˜๋ฉด ๊ณก ์ „ํ™˜ ํ›„ ์•Œ๋ฆผ์„ ๋ฐ›์ง€ ๋ชปํ•จ
- ๋“ฑ๋ก ์ง€์ ์€ ํ•œ ๊ณณ: `currentSong` ๊ตฌ๋…์—์„œ `AVPlayer` ์žฌ์ƒ์„ฑ ์งํ›„ `setUpNotification()` ํ˜ธ์ถœ
- ์ž ๊ธˆํ™”๋ฉด ์•„ํŠธ์›Œํฌ ๋‹ค์šด๋กœ๋“œ๋Š” `backgroundDisposeBag`(๊ณก๋งˆ๋‹ค ์žฌ์ƒ์„ฑ)์— ๋‹ด์•„, ์ด์ „ ๊ณก ์ด๋ฏธ์ง€๊ฐ€ ๋Šฆ๊ฒŒ ๋„์ฐฉํ•ด now playing ์ •๋ณด๋ฅผ ๋ฎ์–ด์“ฐ๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€

## ๋ฐ์ดํ„ฐ ํ๋ฆ„

```
Expand Down
Loading