Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -37,6 +37,11 @@ final class CoreDataStorage {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})

// Background context와 viewContext 자동 병합 설정
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

return container
}()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,15 @@
<relationship name="critters" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ItemEntity" inverseName="userColletion" inverseEntity="ItemEntity"/>
<relationship name="dailyTasks" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="DailyTaskEntity" inverseName="userCollection" inverseEntity="DailyTaskEntity"/>
<relationship name="npcLike" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="NPCLikeEntity" inverseName="userCollection" inverseEntity="NPCLikeEntity"/>
<relationship name="variants" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="VariantCollectionEntity" inverseName="userCollection" inverseEntity="VariantCollectionEntity"/>
<relationship name="villagersHouse" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="VillagersHouseEntity" inverseName="userCollection" inverseEntity="VillagersHouseEntity"/>
<relationship name="villagersLike" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="VillagersLikeEntity" inverseName="userCollection" inverseEntity="VillagersLikeEntity"/>
</entity>
<entity name="VariantCollectionEntity" representedClassName="VariantCollectionEntity" syncable="YES" codeGenerationType="class">
<attribute name="itemName" optional="YES" attributeType="String"/>
<attribute name="variantId" attributeType="String"/>
<relationship name="userCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserCollectionEntity" inverseName="variants" inverseEntity="UserCollectionEntity"/>
</entity>
<entity name="VillagersHouseEntity" representedClassName="VillagersHouseEntity" syncable="YES" codeGenerationType="class">
<attribute name="birthday" attributeType="String"/>
<attribute name="catchphrase" attributeType="String"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ final class CoreDataItemsStorage: ItemsStorage {
self.coreDataStorage = coreDataStorage
}

private func saveViewContext() {
DispatchQueue.main.async {
let viewContext = self.coreDataStorage.persistentContainer.viewContext
do {
try viewContext.save()
} catch {
debugPrint(error)
}
}
}

func fetch() -> Single<[Item]> {
return Single.create { single in
self.coreDataStorage.performBackgroundTask { context in
Expand Down Expand Up @@ -43,7 +54,9 @@ final class CoreDataItemsStorage: ItemsStorage {
let newItem = ItemEntity(item, context: context)
object.addToCritters(newItem)
}

context.saveContext()
self.saveViewContext()
} catch {
debugPrint(error)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// CoreDataVariantsStorage.swift
// Animal-Crossing-Wiki
//
// Created by Claude Code on 2026/02/01.
//

import Foundation
import RxSwift

final class CoreDataVariantsStorage: VariantsStorage {

private let coreDataStorage: CoreDataStorage

init(coreDataStorage: CoreDataStorage = CoreDataStorage.shared) {
self.coreDataStorage = coreDataStorage
}

private func saveViewContext() {
DispatchQueue.main.async {
let viewContext = self.coreDataStorage.persistentContainer.viewContext
do {
try viewContext.save()
} catch {
debugPrint(error)
}
}
}

func fetch() -> Single<Set<String>> {
return Single.create { single in
self.coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let variantEntities = object.variants?.allObjects as? [VariantCollectionEntity] ?? []
let variantIds = Set(variantEntities.compactMap { $0.variantId })
single(.success(variantIds))
} catch {
single(.failure(CoreDataStorageError.readError(error)))
}
}
return Disposables.create()
}
}

func fetchByItem(_ itemName: String) -> Single<Set<String>> {
return Single.create { single in
self.coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let variantEntities = object.variants?.allObjects as? [VariantCollectionEntity] ?? []
let variantIds = Set(variantEntities
.filter { $0.itemName == itemName }
.compactMap { $0.variantId })
single(.success(variantIds))
} catch {
single(.failure(CoreDataStorageError.readError(error)))
}
}
return Disposables.create()
}
}

func fetchAll() -> Single<[String: Set<String>]> {
return Single.create { single in
self.coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let variantEntities = object.variants?.allObjects as? [VariantCollectionEntity] ?? []

var variantsByItem: [String: Set<String>] = [:]
for entity in variantEntities {
guard let itemName = entity.itemName, let variantId = entity.variantId else { continue }
variantsByItem[itemName, default: []].insert(variantId)
}

single(.success(variantsByItem))
} catch {
single(.failure(CoreDataStorageError.readError(error)))
}
}
return Disposables.create()
}
}

func add(_ variantId: String, itemName: String) {
coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let variants = object.variants?.allObjects as? [VariantCollectionEntity] ?? []

// Check if already exists
if variants.contains(where: { $0.variantId == variantId }) {
return
}

let newVariant = VariantCollectionEntity(context: context)
newVariant.variantId = variantId
newVariant.itemName = itemName
object.addToVariants(newVariant)

context.saveContext()
self.saveViewContext()
} catch {
debugPrint(error)
}
}
}

func remove(_ variantId: String) {
coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let variants = object.variants?.allObjects as? [VariantCollectionEntity] ?? []

if let variant = variants.first(where: { $0.variantId == variantId }) {
object.removeFromVariants(variant)

context.saveContext()

DispatchQueue.main.async {
let viewContext = self.coreDataStorage.persistentContainer.viewContext
do {
try viewContext.save()
} catch {
debugPrint(error)
}
}
}
} catch {
debugPrint(error)
}
}
}

func removeAll(for itemName: String) {
coreDataStorage.performBackgroundTask { context in
do {
let object = try self.coreDataStorage.getUserCollection(context)
let allVariants = object.variants?.allObjects as? [VariantCollectionEntity] ?? []
let variantsToRemove = allVariants.filter { $0.itemName == itemName }

variantsToRemove.forEach { variant in
object.removeFromVariants(variant)
}

context.saveContext()
self.saveViewContext()
} catch {
debugPrint(error)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// VariantsStorage.swift
// Animal-Crossing-Wiki
//
// Created by Claude Code on 2026/02/01.
//

import Foundation
import RxSwift

protocol VariantsStorage {
func fetch() -> Single<Set<String>>
func fetchByItem(_ itemName: String) -> Single<Set<String>>
func fetchAll() -> Single<[String: Set<String>]>
func add(_ variantId: String, itemName: String)
func remove(_ variantId: String)
func removeAll(for itemName: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ struct WardrobeVariat: Decodable {
let colors: [Color]

func toVariat() -> Variant {
let generatedVariantId = "\(filename)_\(internalId)_0"

return .init(
image: closetImage ?? storageImage,
variation: nil,
Expand All @@ -79,7 +81,7 @@ struct WardrobeVariat: Decodable {
seasonEventExclusive: seasonEventExclusive,
hhaCategory: nil,
filename: filename,
variantId: "1_0_0",
variantId: generatedVariantId,
internalId: internalId,
variantTranslations: variantTranslations,
colors: colors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import RxSwift
final class ItemDetailViewController: UIViewController {

private let disposeBag = DisposeBag()
private var reactor: ItemDetailReactor?

private lazy var sectionsScrollView: SectionsScrollView = SectionsScrollView()

Expand All @@ -36,6 +37,13 @@ final class ItemDetailViewController: UIViewController {
setUpViews()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 화면이 다시 나타날 때 수집 상태 갱신
reactor?.action.onNext(.fetch)
reactor?.action.onNext(.fetchCollectedVariants)
}

private func setUpViews() {
setUpNavigationItem()
view.backgroundColor = .acBackground
Expand All @@ -56,6 +64,7 @@ final class ItemDetailViewController: UIViewController {
}

func bind(to reactor: ItemDetailReactor) {
self.reactor = reactor
keywordView = ItemKeywordView(item: reactor.currentState.item)
playerView = ItemPlayerView()
navigationItem.title = reactor.currentState.item.translations.localizedName()
Expand All @@ -67,6 +76,11 @@ final class ItemDetailViewController: UIViewController {
reactor.action.onNext(action)
}).disposed(by: disposeBag)

self.rx.viewDidLoad
.map { ItemDetailReactor.Action.fetchCollectedVariants }
.bind(to: reactor.action)
.disposed(by: disposeBag)

checkButton.rx.tap
.map { ItemDetailReactor.Action.check }
.bind(to: reactor.action)
Expand Down Expand Up @@ -105,6 +119,24 @@ final class ItemDetailViewController: UIViewController {
.subscribe(onNext: { [weak self] image in
self?.itemDetailInfoView?.changeImage(image)
}).disposed(by: disposeBag)

itemVariantsColorView?.didToggleVariantCollection
.map { ItemDetailReactor.Action.toggleVariantCollection($0.0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)

itemVariantsPatternView?.didToggleVariantCollection
.map { ItemDetailReactor.Action.toggleVariantCollection($0.0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)

reactor.state.map { $0.collectedVariantIds }
.distinctUntilChanged()
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] collectedIds in
self?.itemVariantsColorView?.updateCollectedVariants(collectedIds)
self?.itemVariantsPatternView?.updateCollectedVariants(collectedIds)
}).disposed(by: disposeBag)
}

private func setUpSection(in item: Item) {
Expand Down Expand Up @@ -133,13 +165,24 @@ final class ItemDetailViewController: UIViewController {
guard Category.furniture().contains(item.category), item.variations != nil else {
return
}
itemVariantsColorView = ItemVariantsView(item: item.variationsWithColor, mode: .color)
itemVariantsPatternView = ItemVariantsView(item: item.variationsWithPattern, mode: .pattern)
let canBodyCustomize = item.bodyCustomize == true
let canPatternCustomize = item.patternCustomize == true

itemVariantsColorView = ItemVariantsView(
item: item.variationsWithColor,
mode: .color,
collectedVariantIds: [],
isReformable: canBodyCustomize
)
itemVariantsPatternView = ItemVariantsView(
item: item.variationsWithPattern,
mode: .pattern,
collectedVariantIds: [],
isReformable: canPatternCustomize
)

let isNoColor = item.variations?.compactMap { $0.filename }.filter { $0.suffix(2) == "_0" }.count ?? 1 <= 1
let isNoPattern = item.patternCustomize == false
let canBodyCustomize = item.bodyCustomize == true
let canPatternCustomize = item.patternCustomize == true
let bodyTitle = "\("Variants".localized) (\(canBodyCustomize ? "Reformable".localized : "Not reformed".localized))"
let patternTitle = "\("Pattern".localized) (\(canPatternCustomize ? "Reformable".localized : "Not reformed".localized))"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ final class CatalogCellReactor: Reactor {
private let item: Item
private let category: Category
private let storage: ItemsStorage
private let variantsStorage: VariantsStorage

init(
item: Item,
category: Category,
state: State,
storage: ItemsStorage = CoreDataItemsStorage()

storage: ItemsStorage = CoreDataItemsStorage(),
variantsStorage: VariantsStorage = CoreDataVariantsStorage()
) {
self.item = item
self.category = category
self.initialState = state
self.storage = storage
self.variantsStorage = variantsStorage
}

func mutate(action: Action) -> Observable<Mutation> {
Expand All @@ -58,8 +60,23 @@ final class CatalogCellReactor: Reactor {

case .check:
HapticManager.shared.impact(style: .medium)
Items.shared.updateItem(item)
storage.update(item)
let willBeUncollected = currentState.isAcquired == true

if willBeUncollected {
let collectedVariants = Items.shared.getCollectedVariants(for: item.name)

variantsStorage.removeAll(for: item.name)
collectedVariants.forEach { variantId in
Items.shared.updateVariant(variantId, itemName: item.name, isAdding: false)
}

Items.shared.updateItem(item)
storage.update(item)
} else {
Items.shared.updateItem(item)
storage.update(item)
}

return .just(.setAcquired(currentState.isAcquired == true ? false : true))
}
}
Expand Down
Loading