From 288f9f0395c5dc71f51e30019bb73ee6b5aa66f2 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 31 Jan 2026 04:42:46 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20diffable=20=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Domain/Sources/Entity/ImageItem.swift | 25 +++++++++++++++++++ .../ClimbRecordDetailViewController.swift | 12 ++++----- .../ClimbRecordDetailViewModel.swift | 16 ++++++------ .../ClimbRecordCollectionViewCell.swift | 18 ++++++------- .../ClimbRecordListViewController.swift | 2 +- .../CustomViews/ImageCollectionViewCell.swift | 5 ++-- 6 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 Domain/Sources/Entity/ImageItem.swift diff --git a/Domain/Sources/Entity/ImageItem.swift b/Domain/Sources/Entity/ImageItem.swift new file mode 100644 index 0000000..58fb72f --- /dev/null +++ b/Domain/Sources/Entity/ImageItem.swift @@ -0,0 +1,25 @@ +// +// ImageItem.swift +// Domain +// +// Created by 김영훈 on 1/31/26. +// + +import Foundation + +public struct ImageItem: Hashable { + let id = UUID() + public let data: Data + + public init(data: Data) { + self.data = data + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: ImageItem, rhs: ImageItem) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift index 514b6ed..e81b996 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift @@ -288,21 +288,21 @@ extension ClimbRecordDetailViewController { case main } - private func createRegistration() -> UICollectionView.CellRegistration { - return UICollectionView.CellRegistration { cell, indexPath, item in + private func createRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in cell.setImage(imageData: item) } } - private func createDataSource() -> UICollectionViewDiffableDataSource { + private func createDataSource() -> UICollectionViewDiffableDataSource { let registration = createRegistration() - return UICollectionViewDiffableDataSource(collectionView: mainView.imageCollectionView) { collectionView, indexPath, item in + return UICollectionViewDiffableDataSource(collectionView: mainView.imageCollectionView) { collectionView, indexPath, item in collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: item) } } - private func applySnapshot(images: [Data]) { - var snapshot = NSDiffableDataSourceSnapshot() + private func applySnapshot(images: [ImageItem]) { + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) snapshot.appendItems(images) dataSource.apply(snapshot, animatingDifferences: true) diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift index 6b5cd8e..4bbca7a 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift @@ -78,7 +78,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { let timelineButtonTitle: AnyPublisher let presentPhotoActionSheet: AnyPublisher let saveCompleted: AnyPublisher - let imagesFetched: AnyPublisher<[Data], Never> + let imagesFetched: AnyPublisher<[ImageItem], Never> let presentImageDeleteAlert: AnyPublisher let placeholderState: AnyPublisher<(isPlaceholder: Bool, text: String), Never> } @@ -91,7 +91,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { let pushVCSubject = PassthroughSubject() let errorMesssageSubject = PassthroughSubject<(String, String), Never>() let presentPhotoActionSheetSubject = PassthroughSubject() - let imagesFetchedSubject = PassthroughSubject<[Data], Never>() + let imagesFetchedSubject = PassthroughSubject<[ImageItem], Never>() let presentImageDeleteAlertSubject = PassthroughSubject() let isPlaceholderSubject = PassthroughSubject() let commentTextSubject = PassthroughSubject() @@ -203,7 +203,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { // Add 화면이면 pendingImages 반환 if isFromAddRecord { - imagesFetchedSubject.send(pendingImages) + imagesFetchedSubject.send(pendingImages.map { ImageItem(data: $0 )}) return } @@ -224,7 +224,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { return nil } } - imagesFetchedSubject.send(imageDatas) + imagesFetchedSubject.send(imageDatas.map { ImageItem(data: $0 )}) } .store(in: &self.cancellables) } @@ -264,7 +264,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { .sink { [weak self] imageData in guard let self else { return } pendingImages.append(imageData) - imagesFetchedSubject.send(pendingImages) + imagesFetchedSubject.send(pendingImages.map { ImageItem(data: $0 )}) } .store(in: &cancellables) @@ -319,7 +319,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { .eraseToAnyPublisher() } .sink { imageDatas in - imagesFetchedSubject.send(imageDatas) + imagesFetchedSubject.send(imageDatas.map { ImageItem(data: $0 )}) } .store(in: &cancellables) @@ -366,7 +366,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { index < pendingImages.count { pendingImages.remove(at: index) } - imagesFetchedSubject.send(pendingImages) + imagesFetchedSubject.send(pendingImages.map { ImageItem(data: $0 )}) } .store(in: &cancellables) @@ -414,7 +414,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { .eraseToAnyPublisher() } .sink { imageDatas in - imagesFetchedSubject.send(imageDatas) + imagesFetchedSubject.send(imageDatas.map { ImageItem(data: $0 )}) } .store(in: &cancellables) diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift index cc1ab38..5d0198b 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift @@ -97,7 +97,7 @@ final class ClimbRecordCollectionViewCell: BaseCollectionViewCell { } // 데이터 정보 표기 - final func setData(_ data: ClimbRecord, isFirstVisit: Bool, imageDatas: [Data] = []) { + final func setData(_ data: ClimbRecord, isFirstVisit: Bool, imageDatas: [ImageItem] = []) { let name = data.mountain.name mountainImageView.image = getMountainImage(date: data.climbDate) @@ -129,7 +129,7 @@ final class ClimbRecordCollectionViewCell: BaseCollectionViewCell { // 로딩 중: placeholder imageCollectionView.isHidden = false commentLabel.isHidden = true - applySnapshot(images: [Data()]) // 빈 Data로 indicator 표시 + applySnapshot(images: [ImageItem(data: Data())]) // 빈 Data로 indicator 표시 } else { // 이미지 있음 imageCollectionView.isHidden = false @@ -315,21 +315,21 @@ extension ClimbRecordCollectionViewCell { return UICollectionViewCompositionalLayout(section: section) } - private func createRegistration() -> UICollectionView.CellRegistration { - return UICollectionView.CellRegistration { cell, indexPath, item in - cell.setImage(imageData: item) + private func createRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + cell.setImage(imageData: item.data) } } - private func createDataSource() -> UICollectionViewDiffableDataSource { + private func createDataSource() -> UICollectionViewDiffableDataSource { let registration = createRegistration() - return UICollectionViewDiffableDataSource(collectionView: imageCollectionView) { collectionView, indexPath, item in + return UICollectionViewDiffableDataSource(collectionView: imageCollectionView) { collectionView, indexPath, item in collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: item) } } - private func applySnapshot(images: [Data]) { - var snapshot = NSDiffableDataSourceSnapshot() + private func applySnapshot(images: [ImageItem]) { + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) snapshot.appendItems(images) dataSource.apply(snapshot, animatingDifferences: false) diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift index 6473a42..ced847b 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift @@ -170,7 +170,7 @@ extension ClimbRecordListViewController: UICollectionViewDelegate, UICollectionV .min { $0.climbDate < $1.climbDate } let isFirstVisit = firstRecordOfMountain?.id == climbRecord.id - let imageDatas = viewModel.recordImageDatas[climbRecord.id] ?? [] + let imageDatas = viewModel.recordImageDatas[climbRecord.id]?.map { ImageItem(data: $0) } ?? [] cell.setImages(row: indexPath.item, total: viewModel.climbRecordList.count) cell.setData(climbRecord, isFirstVisit: isFirstVisit, imageDatas: imageDatas) diff --git a/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift b/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift index c02d782..419b81a 100644 --- a/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift +++ b/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift @@ -6,6 +6,7 @@ // import UIKit +import Domain import SnapKit import Kingfisher @@ -19,8 +20,8 @@ final class ImageCollectionViewCell: BaseCollectionViewCell { return imageView }() - func setImage(imageData: Data) { - if let image = UIImage(data: imageData) { + func setImage(imageData: ImageItem) { + if let image = UIImage(data: imageData.data) { imageView.image = image } else { imageView.image = nil From caa95dca4751eba1930bf3343884fe6281357a30 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 31 Jan 2026 06:25:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?tuist=20=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project.swift | 6 ++++-- Tuist/Config.swift => Tuist.swift | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename Tuist/Config.swift => Tuist.swift (100%) diff --git a/Project.swift b/Project.swift index 2e64e7a..c7e20f2 100644 --- a/Project.swift +++ b/Project.swift @@ -2,7 +2,7 @@ import ProjectDescription let iOSVersion = "16.0" let teamID = "4QUWH828P3" -let appVersion = "1.4.3" +let appVersion = "1.5.0" let buildNumber = "1" let project = Project( @@ -56,7 +56,8 @@ let project = Project( "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib" - ] + ], + basedOnDependencyAnalysis: false ) ], dependencies: [ @@ -73,6 +74,7 @@ let project = Project( "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", "CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION": "YES", "OTHER_LDFLAGS": "$(inherited) -ObjC", + "ENABLE_DEBUG_DYLIB": "NO", "DEVELOPMENT_TEAM": .string(teamID), "MARKETING_VERSION": .string(appVersion), "CURRENT_PROJECT_VERSION": .string(buildNumber) diff --git a/Tuist/Config.swift b/Tuist.swift similarity index 100% rename from Tuist/Config.swift rename to Tuist.swift From 80b47a4e2717b1b4b36e87569ddecf017b118c52 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 31 Jan 2026 06:30:11 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=EC=82=B0=20=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Default/RealmClimbRecordRepositoryImpl.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift b/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift index 7041403..e23cc87 100644 --- a/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift +++ b/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift @@ -185,7 +185,11 @@ public final class RealmClimbRecordRepositoryImpl: ClimbRecordRepository { try realm.write { realm.delete(record.images) if let mountain = record.mountain { - realm.delete(mountain) + let otherRecords = realm.objects(ClimbRecordRealm.self) + .filter("mountain == %@", mountain) + if otherRecords.count <= 1 { + realm.delete(mountain) + } } realm.delete(record.timeLog) realm.delete(record) From 8c28dff354b8385bba02899c0b3a184dd173ed59 Mon Sep 17 00:00:00 2001 From: kyhlsd Date: Sat, 31 Jan 2026 06:53:20 +0900 Subject: [PATCH 4/4] =?UTF-8?q?diffable=20=EC=9D=B4=EC=A4=91=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddClimbRecordViewController.swift | 2 +- .../ClimbRecordDetailViewController.swift | 2 +- .../RecordList/ClimbRecordCollectionViewCell.swift | 2 +- .../Sources/Scenes/MapScene/MapViewController.swift | 2 +- .../Scenes/MeasureScene/MeasureViewController.swift | 2 +- .../Sources/Utils/Extensions/Array+Extension.swift | 13 +++++++++++++ 6 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 Presentation/Sources/Utils/Extensions/Array+Extension.swift diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift index 61a163d..3284f6b 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift @@ -230,7 +230,7 @@ extension AddClimbRecordViewController: UITableViewDelegate { private func applySnapshot(mountains: [Mountain]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(mountains) + snapshot.appendItems(mountains.uniq(by: \.id)) dataSource.apply(snapshot, animatingDifferences: false) } } diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift index e81b996..3d0c786 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift @@ -304,7 +304,7 @@ extension ClimbRecordDetailViewController { private func applySnapshot(images: [ImageItem]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) - snapshot.appendItems(images) + snapshot.appendItems(images.uniq(by: \.self)) dataSource.apply(snapshot, animatingDifferences: true) } } diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift index 5d0198b..8e05ae1 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift @@ -331,7 +331,7 @@ extension ClimbRecordCollectionViewCell { private func applySnapshot(images: [ImageItem]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) - snapshot.appendItems(images) + snapshot.appendItems(images.uniq(by: \.self)) dataSource.apply(snapshot, animatingDifferences: false) } } diff --git a/Presentation/Sources/Scenes/MapScene/MapViewController.swift b/Presentation/Sources/Scenes/MapScene/MapViewController.swift index 2e063e3..81cf784 100644 --- a/Presentation/Sources/Scenes/MapScene/MapViewController.swift +++ b/Presentation/Sources/Scenes/MapScene/MapViewController.swift @@ -215,7 +215,7 @@ extension MapViewController: UICollectionViewDelegate { private func applySnapshot(mountains: [MountainDistance]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) - snapshot.appendItems(mountains) + snapshot.appendItems(mountains.uniq(by: \.self)) dataSource.apply(snapshot, animatingDifferences: true) } diff --git a/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift b/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift index 447827d..9b188f8 100644 --- a/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift +++ b/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift @@ -358,7 +358,7 @@ extension MeasureViewController: UITableViewDelegate { private func applySnapshot(mountains: [MountainInfo]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(mountains) + snapshot.appendItems(mountains.uniq(by: \.id)) dataSource.apply(snapshot, animatingDifferences: false) } } diff --git a/Presentation/Sources/Utils/Extensions/Array+Extension.swift b/Presentation/Sources/Utils/Extensions/Array+Extension.swift new file mode 100644 index 0000000..7668e7a --- /dev/null +++ b/Presentation/Sources/Utils/Extensions/Array+Extension.swift @@ -0,0 +1,13 @@ +// +// Array+Extension.swift +// Presentation +// +// Created by 김영훈 on 1/31/26. +// + +extension Array { + func uniq(by keyPath: KeyPath) -> [Element] { + var set = Set() + return filter { set.insert($0[keyPath: keyPath]).inserted } + } +}