From aa2cd2d69b523ace386bae6582b184f114d1da35 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Fri, 29 May 2026 10:45:50 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat/#333:=20MLSCore=EC=97=90=20DictionaryT?= =?UTF-8?q?abControllable=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccentColor.colorset/Contents.json | 11 ++++++ ...\354\230\244\354\240\204 10_33_07 (1).png" | Bin 0 -> 230338 bytes .../AppIcon.appiconset/Contents.json | 36 ++++++++++++++++++ .../Assets.xcassets/Contents.json | 6 +++ .../Utils/DictionaryTabControllable.swift | 15 ++++++++ 5 files changed, 68 insertions(+) create mode 100644 MLS/MLSBookmarkFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 "MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/ChatGPT Image 2026\353\205\204 5\354\233\224 29\354\235\274 \354\230\244\354\240\204 10_33_07 (1).png" create mode 100644 MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 MLS/MLSBookmarkFeatureExample/Assets.xcassets/Contents.json create mode 100644 MLS/MLSCore/Sources/MLSCore/Utils/DictionaryTabControllable.swift diff --git a/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/ChatGPT Image 2026\353\205\204 5\354\233\224 29\354\235\274 \354\230\244\354\240\204 10_33_07 (1).png" "b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/ChatGPT Image 2026\353\205\204 5\354\233\224 29\354\235\274 \354\230\244\354\240\204 10_33_07 (1).png" new file mode 100644 index 0000000000000000000000000000000000000000..0e39b28aedc24b3d5c2bccaec6f945f46fb77f93 GIT binary patch literal 230338 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

vX&W(v7OwpVP-UbXfi-nKI2^ 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 literal 0 HcmV?d00001 diff --git a/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..6834c3e4 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "filename" : "ChatGPT Image 2026년 5월 29일 오전 10_33_07 (1).png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/MLSBookmarkFeatureExample/Assets.xcassets/Contents.json b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/MLSCore/Sources/MLSCore/Utils/DictionaryTabControllable.swift b/MLS/MLSCore/Sources/MLSCore/Utils/DictionaryTabControllable.swift new file mode 100644 index 00000000..06ad3253 --- /dev/null +++ b/MLS/MLSCore/Sources/MLSCore/Utils/DictionaryTabControllable.swift @@ -0,0 +1,15 @@ +public protocol DictionaryTabControllable: AnyObject { + func changeTab(index: Int) +} + +public enum DictionaryTabRegistry { + private static weak var controller: DictionaryTabControllable? + + public static func register(controller: DictionaryTabControllable) { + self.controller = controller + } + + public static func changeTab(index: Int) { + controller?.changeTab(index: index) + } +} From 1656364df2f8d92f6fa0a9ba48c833a4cb7decb7 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Fri, 29 May 2026 10:45:53 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat/#333:=20MLSBookmarkFeature=20SPM=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contents.xcworkspacedata | 7 + MLS/MLSBookmarkFeature/Package.swift | 81 ++++ .../Data/DTOs/BookmarkDTO.swift | 28 ++ .../Data/DTOs/CollectionListResponseDTO.swift | 17 + .../Data/DTOs/EmptyResponseDTO.swift | 5 + .../Data/DTOs/SetBookmarkDTO.swift | 9 + .../Data/Endpoints/BookmarkEndPoint.swift | 37 ++ .../Data/Endpoints/CollectionEndPoint.swift | 29 ++ .../BookmarkAuthRepositoryImpl.swift | 33 ++ .../Repositories/BookmarkRepositoryImpl.swift | 86 +++++ .../BookmarkUserDefaultsRepositoryImpl.swift | 28 ++ .../CollectionRepositoryImpl.swift | 64 +++ .../AddCollectionFactoryImpl.swift | 19 + .../AddCollection/AddCollectionReactor.swift | 80 ++++ .../AddCollection/AddCollectionView.swift | 171 +++++++++ .../AddCollectionViewController.swift | 209 ++++++++++ .../BookmarkListFactoryImpl.swift | 63 +++ .../BookmarkList/BookmarkListReactor.swift | 239 ++++++++++++ .../BookmarkList/BookmarkListView.swift | 24 ++ .../BookmarkListViewController.swift | 363 ++++++++++++++++++ .../BookmarkMainFactoryImpl.swift | 52 +++ .../BookmarkMain/BookmarkMainReactor.swift | 75 ++++ .../BookmarkMain/BookmarkMainView.swift | 85 ++++ .../BookmarkMainViewController.swift | 240 ++++++++++++ .../BookmarkModalFactoryImpl.swift | 27 ++ .../BookmarkModal/BookmarkModalReactor.swift | 88 +++++ .../BookmarkModal/BookmarkModalView.swift | 91 +++++ .../BookmarkModalViewController.swift | 157 ++++++++ .../BookmarkOnBoardingFactoryImpl.swift | 13 + .../BookmarkOnBoardingReactor.swift | 55 +++ .../BookmarkOnBoardingView.swift | 149 +++++++ .../BookmarkOnBoardingViewController.swift | 64 +++ .../CollectionDetailEmptyView.swift | 66 ++++ .../CollectionDetailFactoryImpl.swift | 59 +++ .../CollectionDetailReactor.swift | 86 +++++ .../CollectionDetailView.swift | 87 +++++ .../CollectionDetailViewController.swift | 209 ++++++++++ .../CollectionEditFactoryImpl.swift | 18 + .../CollectionEditReactor.swift | 63 +++ .../CollectionEdit/CollectionEditView.swift | 92 +++++ .../CollectionEditViewController.swift | 144 +++++++ .../CollectionList/CollectionListCell.swift | 50 +++ .../CollectionListEmptyView.swift | 56 +++ .../CollectionListFactoryImpl.swift | 32 ++ .../CollectionListReactor.swift | 73 ++++ .../CollectionList/CollectionListView.swift | 115 ++++++ .../CollectionListViewController.swift | 159 ++++++++ .../CollectionSettingFactoryImpl.swift | 14 + .../CollectionSettingReactor.swift | 55 +++ .../CollectionSettingView.swift | 40 ++ .../CollectionSettingViewController.swift | 92 +++++ .../Presentation/Common/AddFolderCell.swift | 67 ++++ .../Common/DictionaryListCell.swift | 90 +++++ .../Presentation/Common/FolderCell.swift | 93 +++++ .../Presentation/Common/PageTabbarCell.swift | 38 ++ .../Entities/BookmarkResponse.swift | 17 + .../Entities/CollectionResponse.swift | 13 + .../Entities/CollectionSettingMenu.swift | 24 ++ .../Entities/DictionaryItemType.swift | 17 + .../Entities/DictionaryMainViewType.swift | 16 + .../Entities/DictionaryType.swift | 49 +++ .../Entities/ItemFilterCriteria.swift | 13 + .../Entities/SortType.swift | 37 ++ .../Factories/AddCollectionFactory.swift | 5 + .../Factories/BookmarkListFactory.swift | 5 + .../Factories/BookmarkMainFactory.swift | 7 + .../Factories/BookmarkModalFactory.swift | 6 + .../Factories/BookmarkOnBoardingFactory.swift | 5 + .../Factories/CollectionDetailFactory.swift | 5 + .../Factories/CollectionEditFactory.swift | 5 + .../Factories/CollectionListFactory.swift | 5 + .../Factories/CollectionSettingFactory.swift | 6 + .../DictionaryExternalFactories.swift | 43 +++ .../Repositories/BookmarkAuthRepository.swift | 5 + .../Repositories/BookmarkRepository.swift | 12 + .../BookmarkUserDefaultsRepository.swift | 6 + .../Repositories/CollectionRepository.swift | 10 + .../ParseItemFilterResultUseCase.swift | 3 + .../MockBookmarkAuthRepository.swift | 12 + .../MockBookmarkRepository.swift | 47 +++ .../MockBookmarkUserDefaultsRepository.swift | 17 + .../MockCollectionRepository.swift | 37 ++ .../MockParseItemFilterResultUseCase.swift | 11 + .../MLSBookmarkFeatureTesting/Stubs.swift | 74 ++++ .../BookmarkListReactorTests.swift | 79 ++++ 85 files changed, 5077 insertions(+) create mode 100644 MLS/MLSBookmarkFeature/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 MLS/MLSBookmarkFeature/Package.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/BookmarkDTO.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/CollectionListResponseDTO.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/EmptyResponseDTO.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/SetBookmarkDTO.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/BookmarkEndPoint.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/CollectionEndPoint.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkAuthRepositoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkRepositoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkUserDefaultsRepositoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/CollectionRepositoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailEmptyView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListCell.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListEmptyView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingFactoryImpl.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingReactor.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingView.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingViewController.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/AddFolderCell.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/DictionaryListCell.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/FolderCell.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/PageTabbarCell.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/BookmarkResponse.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionResponse.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionSettingMenu.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryItemType.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryMainViewType.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryType.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/ItemFilterCriteria.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/SortType.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/AddCollectionFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkListFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkMainFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkModalFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkOnBoardingFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionDetailFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionEditFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionListFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionSettingFactory.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/DictionaryExternalFactories.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkAuthRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkUserDefaultsRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/CollectionRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/UseCases/ParseItemFilterResultUseCase.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkAuthRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkUserDefaultsRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockCollectionRepository.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockParseItemFilterResultUseCase.swift create mode 100644 MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift create mode 100644 MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift diff --git a/MLS/MLSBookmarkFeature/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/MLS/MLSBookmarkFeature/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/MLS/MLSBookmarkFeature/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MLS/MLSBookmarkFeature/Package.swift b/MLS/MLSBookmarkFeature/Package.swift new file mode 100644 index 00000000..dda4b20d --- /dev/null +++ b/MLS/MLSBookmarkFeature/Package.swift @@ -0,0 +1,81 @@ +// swift-tools-version: 6.2 +import PackageDescription + +let package = Package( + name: "MLSBookmarkFeature", + platforms: [.iOS(.v15)], + products: [ + .library( + name: "MLSBookmarkFeatureInterface", + targets: ["MLSBookmarkFeatureInterface"] + ), + .library( + name: "MLSBookmarkFeature", + targets: ["MLSBookmarkFeature"] + ), + .library( + name: "MLSBookmarkFeatureTesting", + targets: ["MLSBookmarkFeatureTesting"] + ) + ], + dependencies: [ + .package(path: "../MLSAuthFeature"), + .package(path: "../MLSCore"), + .package(path: "../MLSDesignSystem"), + .package(url: "https://github.com/ReactorKit/ReactorKit.git", from: "3.2.0"), + .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.7.0"), + .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1") + ], + targets: [ + // Interface 모듈 (팩토리/리포지토리 프로토콜 + 엔티티) + .target( + name: "MLSBookmarkFeatureInterface", + dependencies: [ + .product(name: "MLSCore", package: "MLSCore"), + .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), + .product(name: "RxSwift", package: "RxSwift"), + .product(name: "RxRelay", package: "RxSwift") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Feature 모듈 (Presentation + Domain + Data 구현체) + .target( + name: "MLSBookmarkFeature", + dependencies: [ + "MLSBookmarkFeatureInterface", + .product(name: "MLSAuthFeatureInterface", package: "MLSAuthFeature"), + .product(name: "MLSCore", package: "MLSCore"), + .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), + .product(name: "ReactorKit", package: "ReactorKit"), + .product(name: "RxSwift", package: "RxSwift"), + .product(name: "RxCocoa", package: "RxSwift"), + .product(name: "RxRelay", package: "RxSwift"), + .product(name: "SnapKit", package: "SnapKit") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Testing 모듈 (Mock + Stub 객체) + .target( + name: "MLSBookmarkFeatureTesting", + dependencies: [ + "MLSBookmarkFeatureInterface", + .product(name: "MLSAuthFeatureInterface", package: "MLSAuthFeature"), + .product(name: "MLSCore", package: "MLSCore"), + .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), + .product(name: "RxSwift", package: "RxSwift"), + .product(name: "RxRelay", package: "RxSwift") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Tests 모듈 + .testTarget( + name: "MLSBookmarkFeatureTests", + dependencies: [ + "MLSBookmarkFeature", + "MLSBookmarkFeatureInterface", + "MLSBookmarkFeatureTesting", + .product(name: "RxBlocking", package: "RxSwift") + ] + ) + ] +) diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/BookmarkDTO.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/BookmarkDTO.swift new file mode 100644 index 00000000..f5f9765b --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/BookmarkDTO.swift @@ -0,0 +1,28 @@ +import MLSBookmarkFeatureInterface + +struct BookmarkDTO: Decodable { + let bookmarkId: Int + let originalId: Int + let name: String + let imageUrl: String + let type: String + let level: Int? + + func toDomain() -> BookmarkResponse? { + guard let type = DictionaryItemType(rawValue: type) else { return nil } + return BookmarkResponse( + name: name, + bookmarkId: bookmarkId, + originalId: originalId, + imageUrl: imageUrl, + type: type, + level: level + ) + } +} + +extension Array where Element == BookmarkDTO { + func toDomain() -> [BookmarkResponse] { + return self.compactMap { $0.toDomain() } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/CollectionListResponseDTO.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/CollectionListResponseDTO.swift new file mode 100644 index 00000000..b3494c56 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/CollectionListResponseDTO.swift @@ -0,0 +1,17 @@ +import MLSBookmarkFeatureInterface + +struct CollectionListResponseDTO: Decodable { + let collectionId: Int + let name: String + let createdAt: [Int] + let recentBookmarks: [BookmarkDTO] + + func toDomain() -> CollectionResponse { + return CollectionResponse( + collectionId: collectionId, + name: name, + createdAt: createdAt, + recentBookmarks: recentBookmarks.toDomain() + ) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/EmptyResponseDTO.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/EmptyResponseDTO.swift new file mode 100644 index 00000000..fc48e0bd --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/EmptyResponseDTO.swift @@ -0,0 +1,5 @@ +struct EmptyResponseDTO: Decodable { + func toBookmarkDomain() -> Int? { + return nil + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/SetBookmarkDTO.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/SetBookmarkDTO.swift new file mode 100644 index 00000000..676b62bf --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/DTOs/SetBookmarkDTO.swift @@ -0,0 +1,9 @@ +struct SetBookmarkDTO: Decodable { + let bookmarkId: Int + let bookmarkType: String + let resourceId: Int + + func toDomain() -> Int { + return self.bookmarkId + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/BookmarkEndPoint.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/BookmarkEndPoint.swift new file mode 100644 index 00000000..ca79ae5d --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/BookmarkEndPoint.swift @@ -0,0 +1,37 @@ +import MLSCore + +enum BookmarkEndPoint { + private static let base = "https://mapleland.2megabytes.me" + + static func setBookmark(body: Encodable) -> ResponsableEndPoint { + .init(baseURL: base, path: "/api/v1/bookmarks", method: .POST, body: body) + } + + static func deleteBookmark(bookmarkId: Int) -> ResponsableEndPoint { + .init(baseURL: base, path: "/api/v1/bookmarks/\(bookmarkId)", method: .DELETE) + } + + static func fetchBookmark(query: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/bookmarks", method: .GET, query: query) + } + + static func fetchMonsterBookmark(query: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/bookmarks/monsters", method: .GET, query: query) + } + + static func fetchNPCBookmark(query: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/bookmarks/npcs", method: .GET, query: query) + } + + static func fetchQuestBookmark(query: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/bookmarks/quests", method: .GET, query: query) + } + + static func fetchItemBookmark(query: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/bookmarks/items", method: .GET, query: query) + } + + static func fetchMapBookmark(query: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/bookmarks/maps", method: .GET, query: query) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/CollectionEndPoint.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/CollectionEndPoint.swift new file mode 100644 index 00000000..a5821920 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Endpoints/CollectionEndPoint.swift @@ -0,0 +1,29 @@ +import MLSCore + +enum CollectionEndPoint { + private static let base = "https://mapleland.2megabytes.me" + + static func fetchCollectionList(query: Encodable) -> ResponsableEndPoint<[CollectionListResponseDTO]> { + .init(baseURL: base, path: "/api/v1/collections", method: .GET, query: query) + } + + static func createCollectionList(body: Encodable) -> EndPoint { + .init(baseURL: base, path: "/api/v1/collections", method: .POST, body: body) + } + + static func fetchCollection(id: Int) -> ResponsableEndPoint<[BookmarkDTO]> { + .init(baseURL: base, path: "/api/v1/collections/\(id)/bookmarks", method: .GET) + } + + static func setCollectionName(id: Int, body: Encodable) -> EndPoint { + .init(baseURL: base, path: "/api/v1/collections/\(id)", method: .PUT, body: body) + } + + static func deleteCollection(id: Int) -> EndPoint { + .init(baseURL: base, path: "/api/v1/collections/\(id)", method: .DELETE) + } + + static func addCollectionAndBookmark(body: Encodable) -> EndPoint { + .init(baseURL: base, path: "/api/v1/bookmark-collections", method: .POST, body: body) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkAuthRepositoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkAuthRepositoryImpl.swift new file mode 100644 index 00000000..2905423d --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkAuthRepositoryImpl.swift @@ -0,0 +1,33 @@ +import MLSAuthFeatureInterface +import MLSBookmarkFeatureInterface +import RxSwift + +final class BookmarkAuthRepositoryImpl: BookmarkAuthRepository { + private let tokenRepository: TokenRepository + private let authAPIRepository: AuthAPIRepository + + init(tokenRepository: TokenRepository, authAPIRepository: AuthAPIRepository) { + self.tokenRepository = tokenRepository + self.authAPIRepository = authAPIRepository + } + + func isLoggedIn() -> Observable { + switch tokenRepository.fetchToken(type: .refreshToken) { + case .success(let token): + guard !token.isEmpty else { return .just(false) } + return authAPIRepository.reissueToken(refreshToken: token) + .map { [weak self] response -> Bool in + guard let self else { return false } + let accessResult = self.tokenRepository.saveToken(type: .accessToken, value: response.accessToken) + let refreshResult = self.tokenRepository.saveToken(type: .refreshToken, value: response.refreshToken) + switch (accessResult, refreshResult) { + case (.success, .success): return true + default: return false + } + } + .catch { _ in .just(false) } + case .failure: + return .just(false) + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkRepositoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkRepositoryImpl.swift new file mode 100644 index 00000000..49f33429 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkRepositoryImpl.swift @@ -0,0 +1,86 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import RxSwift + +final class BookmarkRepositoryImpl: BookmarkRepository { + private let provider: NetworkProvider + private let interceptor: Interceptor + + init() { + self.provider = NetworkProviderImpl() + self.interceptor = TokenInterceptor() + } + + func setBookmark(resourceId: Int, type: DictionaryItemType) -> Observable { + let endpoint = BookmarkEndPoint.setBookmark(body: SetBookmarkBody(bookmarkType: type.rawValue, resourceId: resourceId)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func deleteBookmark(bookmarkId: Int) -> Observable { + let endpoint = BookmarkEndPoint.deleteBookmark(bookmarkId: bookmarkId) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toBookmarkDomain() } + } + + func fetchBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + let endpoint = BookmarkEndPoint.fetchBookmark(query: SortQuery(sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func fetchMonsterBookmark(minLevel: Int?, maxLevel: Int?, sort: String?) -> Observable<[BookmarkResponse]> { + let endpoint = BookmarkEndPoint.fetchMonsterBookmark(query: MonsterQuery(minLevel: minLevel, maxLevel: maxLevel, sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func fetchNPCBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + let endpoint = BookmarkEndPoint.fetchNPCBookmark(query: SortQuery(sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func fetchQuestBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + let endpoint = BookmarkEndPoint.fetchQuestBookmark(query: SortQuery(sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func fetchItemBookmark(jobId: Int?, minLevel: Int?, maxLevel: Int?, categoryIds: [Int]?, sort: String?) -> Observable<[BookmarkResponse]> { + let endpoint = BookmarkEndPoint.fetchItemBookmark(query: ItemQuery(jobId: jobId, minLevel: minLevel, maxLevel: maxLevel, categoryIds: categoryIds, sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func fetchMapBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + let endpoint = BookmarkEndPoint.fetchMapBookmark(query: SortQuery(sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } +} + +private extension BookmarkRepositoryImpl { + struct SortQuery: Encodable { + let sort: String? + } + + struct SetBookmarkBody: Encodable { + let bookmarkType: String + let resourceId: Int + } + + struct MonsterQuery: Encodable { + let minLevel: Int? + let maxLevel: Int? + let sort: String? + } + + struct ItemQuery: Encodable { + let jobId: Int? + let minLevel: Int? + let maxLevel: Int? + let categoryIds: [Int]? + let sort: String? + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkUserDefaultsRepositoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkUserDefaultsRepositoryImpl.swift new file mode 100644 index 00000000..8176ca4b --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/BookmarkUserDefaultsRepositoryImpl.swift @@ -0,0 +1,28 @@ +import Foundation +import MLSBookmarkFeatureInterface +import RxSwift + +final class BookmarkUserDefaultsRepositoryImpl: BookmarkUserDefaultsRepository { + private let key = "bookmark_onboarding_visited" + + func hasVisitedOnboarding() -> Observable { + let hasVisited = UserDefaults.standard.bool(forKey: key) + if !hasVisited { + UserDefaults.standard.set(true, forKey: key) + return .just(false) + } + return .just(true) + } + + func markOnboardingVisited() -> Completable { + return Completable.create { [weak self] observer in + guard let self else { + observer(.completed) + return Disposables.create() + } + UserDefaults.standard.set(true, forKey: self.key) + observer(.completed) + return Disposables.create() + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/CollectionRepositoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/CollectionRepositoryImpl.swift new file mode 100644 index 00000000..903e6efd --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Data/Repositories/CollectionRepositoryImpl.swift @@ -0,0 +1,64 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import RxSwift + +final class CollectionRepositoryImpl: CollectionRepository { + private let provider: NetworkProvider + private let interceptor: Interceptor + + init() { + self.provider = NetworkProviderImpl() + self.interceptor = TokenInterceptor() + } + + func fetchCollectionList(sort: String?) -> Observable<[CollectionResponse]> { + let endpoint = CollectionEndPoint.fetchCollectionList(query: FetchListQuery(sort: sort)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.map { $0.toDomain() } } + } + + func createCollectionList(name: String) -> Completable { + let endpoint = CollectionEndPoint.createCollectionList(body: CreateBody(name: name)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + } + + func fetchCollection(id: Int) -> Observable<[BookmarkResponse]> { + let endpoint = CollectionEndPoint.fetchCollection(id: id) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.toDomain() } + } + + func updateCollectionName(collectionId: Int, name: String) -> Completable { + let endpoint = CollectionEndPoint.setCollectionName(id: collectionId, body: UpdateNameBody(name: name)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + } + + func deleteCollection(collectionId: Int) -> Completable { + let endpoint = CollectionEndPoint.deleteCollection(id: collectionId) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + } + + func addCollectionAndBookmark(collectionIds: [Int], bookmarkIds: [Int]) -> Completable { + let endpoint = CollectionEndPoint.addCollectionAndBookmark(body: AddBody(collectionIds: collectionIds, bookmarkIds: bookmarkIds)) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + } +} + +private extension CollectionRepositoryImpl { + struct FetchListQuery: Encodable { + let sort: String? + } + + struct CreateBody: Encodable { + let name: String + } + + struct UpdateNameBody: Encodable { + let name: String + } + + struct AddBody: Encodable { + let collectionIds: [Int] + let bookmarkIds: [Int] + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionFactoryImpl.swift new file mode 100644 index 00000000..70bc2ba6 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionFactoryImpl.swift @@ -0,0 +1,19 @@ +import MLSBookmarkFeatureInterface +import MLSCore + +public final class AddCollectionFactoryImpl: AddCollectionFactory { + private let collectionRepository: CollectionRepository + + public init(collectionRepository: CollectionRepository) { + self.collectionRepository = collectionRepository + } + + public func make(collection: CollectionResponse?) -> BaseViewController { + let viewController = AddCollectionViewController() + viewController.reactor = AddCollectionReactor( + collection: collection, + collectionRepository: collectionRepository + ) + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionReactor.swift new file mode 100644 index 00000000..15b8f3a0 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionReactor.swift @@ -0,0 +1,80 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class AddCollectionReactor: Reactor { + enum Route { + case dismiss + case dismissWithData + case createError + case updateError + } + + enum Action { + case inputTextChanged(String?) + case backButtonTapped + case completeButtonTapped + } + + enum Mutation { + case saveInput(String) + case setError(Bool) + case setButtonEnabled(Bool) + case toNavigate(Route) + } + + struct State { + @Pulse var route: Route? + var collection: CollectionResponse? + var inputText: String? + var isError: Bool = false + var isButtonEnabled: Bool = false + } + + var initialState: State + private let collectionRepository: CollectionRepository + + init(collection: CollectionResponse?, collectionRepository: CollectionRepository) { + self.initialState = State(collection: collection, inputText: collection?.name) + self.collectionRepository = collectionRepository + } + + func mutate(action: Action) -> Observable { + switch action { + case .inputTextChanged(let text): + let trimmed = text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return Observable.from([.setButtonEnabled(!trimmed.isEmpty), .saveInput(trimmed)]) + + case .backButtonTapped: + return .just(.toNavigate(.dismiss)) + + case .completeButtonTapped: + guard let text = currentState.inputText else { return .empty() } + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.count > 18 { + return .just(.setError(true)) + } + if currentState.collection == nil { + return collectionRepository.createCollectionList(name: trimmed) + .andThen(.just(.toNavigate(.dismissWithData))) + .catch { _ in .just(.toNavigate(.createError)) } + } else { + guard let id = currentState.collection?.collectionId else { return .empty() } + return collectionRepository.updateCollectionName(collectionId: id, name: trimmed) + .andThen(.just(.toNavigate(.dismissWithData))) + .catch { _ in .just(.toNavigate(.updateError)) } + } + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .saveInput(let text): newState.inputText = text + case .setError(let isError): newState.isError = isError + case .setButtonEnabled(let isEnabled): newState.isButtonEnabled = isEnabled + case .toNavigate(let route): newState.route = route + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionView.swift new file mode 100644 index 00000000..700a0830 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionView.swift @@ -0,0 +1,171 @@ +import MLSBookmarkFeatureInterface +import MLSDesignSystem +import SnapKit +import UIKit + +final class AddCollectionView: UIView { + enum Constant { + static let radius: CGFloat = 8 + static let titleTopMargin: CGFloat = 20 + static let imageViewSize: CGFloat = 40 + static let iconInset: CGFloat = 8 + static let inputInset: CGFloat = 10 + static let inputSpacing: CGFloat = 16 + static let inputHeight: CGFloat = 60 + static let horizontalMargin: CGFloat = 16 + static let nameLabelTopMargin: CGFloat = 20 + static let nameLabelBottomMargin: CGFloat = 14 + static let buttonTopMargin: CGFloat = 68 + static let buttonBottomMargin: CGFloat = 10 + } + + var addButtonBottomConstraint: Constraint? + + private let titleLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .h_xl_b, text: "컬렉션", alignment: .left) + return label + }() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(DesignSystemAsset.image(named: "largeX"), for: .normal) + return button + }() + + private lazy var inputTextView: UIView = { + let view = UIView() + view.backgroundColor = .neutral100 + view.layer.cornerRadius = Constant.radius + view.addSubview(imageView) + view.addSubview(inputTextField) + + imageView.snp.makeConstraints { make in + make.verticalEdges.leading.equalToSuperview().inset(Constant.inputInset) + make.size.equalTo(Constant.imageViewSize) + } + + inputTextField.snp.makeConstraints { make in + make.centerY.trailing.equalToSuperview() + make.leading.equalTo(imageView.snp.trailing).offset(Constant.inputSpacing) + } + return view + }() + + private var nameLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .sub_m_sb, text: "컬렉션 이름 입력", alignment: .left) + return label + }() + + private lazy var imageView: UIView = { + let view = UIView() + view.layer.cornerRadius = Constant.radius + view.backgroundColor = .neutral200 + view.addSubview(iconView) + iconView.snp.makeConstraints { make in + make.center.equalTo(view).inset(Constant.iconInset) + } + return view + }() + + private let iconView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "bookmark").withRenderingMode(.alwaysTemplate) + view.tintColor = .neutral300 + return view + }() + + let inputTextField: UITextField = { + let textField = UITextField() + textField.backgroundColor = .clearMLS + textField.tintColor = .primary300 + textField.textAlignment = .left + textField.font = .korFont(style: .semiBold, size: 14) + textField.attributedPlaceholder = NSAttributedString( + string: "컬렉션 이름을 입력해주세요 (최대 18자)", + attributes: [.foregroundColor: UIColor.neutral500] + ) + return textField + }() + + private let errorMessage = ErrorMessage(message: "폴더명은 18자 이하로 입력해주세요.") + let completeButton = CommonButton(style: .normal, title: "완료", disabledTitle: "완료") + + init() { + super.init(frame: .zero) + addViews() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +private extension AddCollectionView { + func addViews() { + addSubview(titleLabel) + addSubview(backButton) + addSubview(nameLabel) + addSubview(inputTextView) + addSubview(errorMessage) + addSubview(completeButton) + } + + func setupConstraints() { + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(Constant.titleTopMargin) + $0.leading.equalToSuperview().inset(Constant.horizontalMargin) + } + + backButton.snp.makeConstraints { + $0.top.equalTo(titleLabel) + $0.leading.equalTo(titleLabel.snp.trailing) + $0.trailing.equalToSuperview().inset(Constant.horizontalMargin) + } + + nameLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(Constant.nameLabelTopMargin) + $0.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + } + + inputTextView.snp.makeConstraints { + $0.top.equalTo(nameLabel.snp.bottom).offset(Constant.nameLabelBottomMargin) + $0.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + $0.height.equalTo(Constant.inputHeight) + } + + errorMessage.snp.makeConstraints { + $0.bottom.equalTo(completeButton.snp.top).offset(-Constant.buttonBottomMargin) + $0.centerX.equalToSuperview() + } + + completeButton.snp.makeConstraints { + $0.top.equalTo(inputTextView.snp.bottom).offset(Constant.buttonTopMargin) + $0.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + addButtonBottomConstraint = $0.bottom.equalToSuperview().inset(Constant.buttonBottomMargin).constraint + } + } +} + +extension AddCollectionView { + func setError(isError: Bool) { + errorMessage.isHidden = !isError + } + + func setButtonEnabled(isEnabled: Bool) { + completeButton.isEnabled = isEnabled + } + + func checkIsEmptyCollection(collection: CollectionResponse?) { + if collection != nil { + nameLabel.attributedText = .makeStyledString(font: .sub_m_sb, text: "컬렉션 이름 수정", alignment: .left) + } + } + + func updateTextField(text: String?) { + if let text { + inputTextField.attributedText = .makeStyledString(font: .b_s_sb, text: text, color: .textColor, alignment: .left) + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionViewController.swift new file mode 100644 index 00000000..a82dcf12 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/AddCollection/AddCollectionViewController.swift @@ -0,0 +1,209 @@ +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxSwift +import SnapKit +import UIKit + +final class AddCollectionViewController: BaseViewController, View { + typealias Reactor = AddCollectionReactor + + var disposeBag = DisposeBag() + let dismissed = PublishSubject() + + private let mainView = AddCollectionView() + private let addCollectionContainer = UIView() + private let dimmedBackgroundView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.black.withAlphaComponent(0.5) + view.alpha = 0 + view.isHidden = true + view.isUserInteractionEnabled = true + return view + }() + + override init() { + super.init() + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + setupConstraints() + setupGestures() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + setupKeyboard() + presentWithAnimation() + } +} + +private extension AddCollectionViewController { + func setupViews() { + view.backgroundColor = .clear + view.addSubview(dimmedBackgroundView) + view.addSubview(addCollectionContainer) + addCollectionContainer.addSubview(mainView) + + addCollectionContainer.layer.cornerRadius = 16 + addCollectionContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + addCollectionContainer.backgroundColor = .whiteMLS + addCollectionContainer.isHidden = true + } + + func setupConstraints() { + dimmedBackgroundView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + + addCollectionContainer.snp.makeConstraints { make in + make.horizontalEdges.bottom.equalTo(view.safeAreaLayoutGuide) + } + + mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func setupGestures() { + let tapGesture = UITapGestureRecognizer() + dimmedBackgroundView.addGestureRecognizer(tapGesture) + tapGesture.rx.event + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.dismissWithAnimation(withData: false) + }) + .disposed(by: disposeBag) + } + + func setupKeyboard() { + setupKeyboard(inset: AddCollectionView.Constant.buttonBottomMargin) { [weak self] height in + self?.mainView.addButtonBottomConstraint?.update(inset: height) + } + } +} + +extension AddCollectionViewController { + func bind(reactor: Reactor) { + mainView.inputTextField.rx.text + .distinctUntilChanged() + .map { Reactor.Action.inputTextChanged($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.backButton.rx.tap + .map { Reactor.Action.backButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.completeButton.rx.tap + .map { Reactor.Action.completeButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .map(\.inputText) + .take(1) + .withUnretained(self) + .bind(onNext: { owner, text in + owner.mainView.updateTextField(text: text) + }) + .disposed(by: disposeBag) + + reactor.state + .map(\.isError) + .distinctUntilChanged() + .withUnretained(self) + .bind { owner, isError in + owner.mainView.setError(isError: isError) + } + .disposed(by: disposeBag) + + reactor.state + .map(\.collection) + .distinctUntilChanged() + .withUnretained(self) + .bind { owner, collection in + owner.mainView.checkIsEmptyCollection(collection: collection) + } + .disposed(by: disposeBag) + + reactor.state + .map(\.isButtonEnabled) + .distinctUntilChanged() + .withUnretained(self) + .bind { owner, isEnabled in + owner.mainView.setButtonEnabled(isEnabled: isEnabled) + } + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .observe(on: MainScheduler.asyncInstance) + .withUnretained(self) + .subscribe(onNext: { owner, route in + switch route { + case .dismiss: + owner.dismissWithAnimation(withData: false) { + owner.dismiss(animated: false) + } + case .dismissWithData: + owner.dismissWithAnimation(withData: true) { + owner.dismiss(animated: false) + } + case .updateError: + ToastFactory.createToast(message: "컬렉션 수정에 실패했어요. 다시 시도해주세요.") + case .createError: + ToastFactory.createToast(message: "컬렉션 생성에 실패했어요. 다시 시도해주세요.") + default: + break + } + }) + .disposed(by: disposeBag) + } +} + +private extension AddCollectionViewController { + func presentWithAnimation() { + dimmedBackgroundView.alpha = 0 + dimmedBackgroundView.isHidden = false + addCollectionContainer.isHidden = false + addCollectionContainer.transform = CGAffineTransform(translationX: 0, y: 400) + + UIView.animate(withDuration: 0.25) { + self.dimmedBackgroundView.alpha = 1 + self.addCollectionContainer.transform = .identity + } + + mainView.setError(isError: false) + mainView.setButtonEnabled(isEnabled: false) + mainView.inputTextField.becomeFirstResponder() + } + + func dismissWithAnimation(withData: Bool, completion: (() -> Void)? = nil) { + mainView.endEditing(true) + UIView.animate(withDuration: 0.25, animations: { + self.dimmedBackgroundView.alpha = 0 + self.addCollectionContainer.transform = CGAffineTransform(translationX: 0, y: 400) + }, completion: { _ in + self.addCollectionContainer.isHidden = true + self.dimmedBackgroundView.isHidden = true + + if withData { + guard let text = self.mainView.inputTextField.text else { return } + self.dismissed.onNext(text) + self.dismissed.onCompleted() + } + + self.dismiss(animated: false, completion: completion) + }) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListFactoryImpl.swift new file mode 100644 index 00000000..ad3f231a --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListFactoryImpl.swift @@ -0,0 +1,63 @@ +import MLSAuthFeatureInterface +import MLSBookmarkFeatureInterface +import MLSCore + +public final class BookmarkListFactoryImpl: BookmarkListFactory { + private let itemFilterFactory: ItemFilterBottomSheetFactory + private let monsterFilterFactory: MonsterFilterBottomSheetFactory + private let sortedFactory: SortedBottomSheetFactory + private let bookmarkModalFactory: BookmarkModalFactory + private let loginFactory: LoginFactory + private let dictionaryDetailFactory: DictionaryDetailFactory + private let collectionEditFactory: CollectionEditFactory + private let authRepository: BookmarkAuthRepository + private let bookmarkRepository: BookmarkRepository + private let parseItemFilterResultUseCase: ParseItemFilterResultUseCase + + public init( + itemFilterFactory: ItemFilterBottomSheetFactory, + monsterFilterFactory: MonsterFilterBottomSheetFactory, + sortedFactory: SortedBottomSheetFactory, + bookmarkModalFactory: BookmarkModalFactory, + loginFactory: LoginFactory, + dictionaryDetailFactory: DictionaryDetailFactory, + collectionEditFactory: CollectionEditFactory, + authRepository: BookmarkAuthRepository, + bookmarkRepository: BookmarkRepository, + parseItemFilterResultUseCase: ParseItemFilterResultUseCase + ) { + self.itemFilterFactory = itemFilterFactory + self.monsterFilterFactory = monsterFilterFactory + self.sortedFactory = sortedFactory + self.bookmarkModalFactory = bookmarkModalFactory + self.loginFactory = loginFactory + self.dictionaryDetailFactory = dictionaryDetailFactory + self.collectionEditFactory = collectionEditFactory + self.authRepository = authRepository + self.bookmarkRepository = bookmarkRepository + self.parseItemFilterResultUseCase = parseItemFilterResultUseCase + } + + public func make(type: DictionaryType, listType: DictionaryMainViewType) -> BaseViewController { + let reactor = BookmarkListReactor( + type: type, + authRepository: authRepository, + bookmarkRepository: bookmarkRepository, + parseItemFilterResultUseCase: parseItemFilterResultUseCase + ) + let viewController = BookmarkListViewController( + reactor: reactor, + itemFilterFactory: itemFilterFactory, + monsterFilterFactory: monsterFilterFactory, + sortedFactory: sortedFactory, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory, + dictionaryDetailFactory: dictionaryDetailFactory, + collectionEditFactory: collectionEditFactory + ) + if listType == .search { + viewController.isBottomTabbarHidden = true + } + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift new file mode 100644 index 00000000..9a59aa44 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift @@ -0,0 +1,239 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class BookmarkListReactor: Reactor { + enum Route { + case none + case sort(DictionaryType) + case filter(DictionaryType) + case detail(DictionaryType, Int) + case dictionary + case login + case edit + case bookmarkError + } + + enum UIEvent { + case none + case add(BookmarkResponse) + case delete(BookmarkResponse) + case undo + case login + } + + enum ViewState: Equatable { + case loginWithData + case loginWithoutData + case logout + } + + enum Action { + case viewWillAppear + case toggleBookmark(Int) + case sortButtonTapped + case filterButtonTapped + case editButtonTapped + case fetchList + case sortOptionSelected(SortType) + case filterOptionSelected(startLevel: Int, endLevel: Int) + case undoLastDeletedBookmark + case dataTapped(Int) + case emptyButtonTapped + case itemFilterOptionSelected([(String, String)]) + case showLogin + } + + enum Mutation { + case setItems([BookmarkResponse]) + case setLoginState(Bool) + case setSort(SortType) + case setFilter(start: Int?, end: Int?) + case setLastDeletedBookmark(BookmarkResponse?) + case navigateTo(Route) + case setJobId([Int]) + case setCategoryId([Int]) + case setEvent(UIEvent) + } + + struct State { + @Pulse var uiEvent: UIEvent = .none + @Pulse var route: Route + var items: [BookmarkResponse] = [] + var type: DictionaryType + var isLogin: Bool + var jobId: [Int]? + var categoryIds: [Int]? + var sort: SortType? + var startLevel: Int? + var endLevel: Int? + var lastDeletedBookmark: BookmarkResponse? + var viewState: ViewState { + if !isLogin { return .logout } + else if items.isEmpty { return .loginWithoutData } + else { return .loginWithData } + } + } + + var initialState: State + + private let authRepository: BookmarkAuthRepository + private let bookmarkRepository: BookmarkRepository + private let parseItemFilterResultUseCase: ParseItemFilterResultUseCase + + init( + type: DictionaryType, + authRepository: BookmarkAuthRepository, + bookmarkRepository: BookmarkRepository, + parseItemFilterResultUseCase: ParseItemFilterResultUseCase + ) { + self.initialState = State(route: .none, type: type, isLogin: false) + self.authRepository = authRepository + self.bookmarkRepository = bookmarkRepository + self.parseItemFilterResultUseCase = parseItemFilterResultUseCase + } + + func mutate(action: Action) -> Observable { + switch action { + case .viewWillAppear: + return authRepository.isLoggedIn() + .flatMap { [weak self] isLogin -> Observable in + guard let self else { return .empty() } + if !isLogin { + return .just(.setLoginState(false)) + } else { + return Observable.concat([.just(.setLoginState(true)), self.fetchList()]) + } + } + + case let .toggleBookmark(id): + return handleToggle(id: id) + + case .sortButtonTapped: + return .just(.navigateTo(.sort(currentState.type))) + + case .filterButtonTapped: + return .just(.navigateTo(.filter(currentState.type))) + + case .fetchList: + guard currentState.isLogin else { return .empty() } + return fetchList() + + case let .sortOptionSelected(sort): + return Observable.concat([.just(.setSort(sort)), fetchList(sort: sort)]) + + case let .filterOptionSelected(startLevel, endLevel): + return Observable.concat([.just(.setFilter(start: startLevel, end: endLevel)), fetchList()]) + + case .undoLastDeletedBookmark: + return handleUndo() + + case let .dataTapped(index): + let item = currentState.items[index] + guard let type = item.type.toDictionaryType else { return .empty() } + return .just(.navigateTo(.detail(type, item.originalId))) + + case .emptyButtonTapped: + if currentState.viewState == .logout { + return .just(.navigateTo(.login)) + } else { + return .just(.navigateTo(.dictionary)) + } + + case .editButtonTapped: + return .just(.navigateTo(.edit)) + + case let .itemFilterOptionSelected(results): + let criteria = parseItemFilterResultUseCase.execute(results: results) + return Observable.concat([ + .just(.setJobId(criteria.jobIds)), + .just(.setFilter(start: criteria.startLevel, end: criteria.endLevel)), + .just(.setCategoryId(criteria.categoryIds)) + ]) + .concat(Observable.deferred { [weak self] in + guard let self else { return .empty() } + return self.fetchList() + }) + + case .showLogin: + return .just(.setEvent(.login)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setItems(response): newState.items = response + case let .setLoginState(isLogin): newState.isLogin = isLogin + case let .setSort(sort): newState.sort = sort + case let .setFilter(start, end): + newState.startLevel = start + newState.endLevel = end + case let .setLastDeletedBookmark(item): newState.lastDeletedBookmark = item + case let .navigateTo(route): newState.route = route + case let .setJobId(ids): newState.jobId = ids + case let .setCategoryId(ids): newState.categoryIds = ids + case let .setEvent(event): newState.uiEvent = event + } + return newState + } +} + +private extension BookmarkListReactor { + func fetchList(sort: SortType? = nil) -> Observable { + let resolvedSort = (sort ?? currentState.sort)?.sortParameter + switch currentState.type { + case .total: + return bookmarkRepository.fetchBookmark(sort: resolvedSort).map { .setItems($0) } + case .monster: + return bookmarkRepository.fetchMonsterBookmark( + minLevel: currentState.startLevel ?? 1, + maxLevel: currentState.endLevel ?? 200, + sort: resolvedSort + ).map { .setItems($0) } + case .item: + return bookmarkRepository.fetchItemBookmark( + jobId: nil, + minLevel: currentState.startLevel, + maxLevel: currentState.endLevel, + categoryIds: nil, + sort: resolvedSort + ).map { .setItems($0) } + case .npc: + return bookmarkRepository.fetchNPCBookmark(sort: resolvedSort).map { .setItems($0) } + case .quest: + return bookmarkRepository.fetchQuestBookmark(sort: resolvedSort).map { .setItems($0) } + case .map: + return bookmarkRepository.fetchMapBookmark(sort: resolvedSort).map { .setItems($0) } + default: + return .empty() + } + } + + func handleToggle(id: Int) -> Observable { + guard let index = currentState.items.firstIndex(where: { $0.originalId == id }) else { + return .empty() + } + let targetItem = currentState.items[index] + return bookmarkRepository.deleteBookmark(bookmarkId: targetItem.bookmarkId) + .flatMap { [self] _ -> Observable in + return Observable.concat([ + .from([.setLastDeletedBookmark(targetItem), .setEvent(.delete(targetItem))]), + self.fetchList() + ]) + } + .catch { _ in .just(.navigateTo(.bookmarkError)) } + } + + func handleUndo() -> Observable { + guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } + return bookmarkRepository.setBookmark(resourceId: lastDeleted.originalId, type: lastDeleted.type) + .flatMap { [self] _ -> Observable in + return Observable.concat([ + .from([.setLastDeletedBookmark(nil), .setEvent(.add(lastDeleted))]), + self.fetchList() + ]) + } + .catch { _ in .just(.navigateTo(.bookmarkError)) } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListView.swift new file mode 100644 index 00000000..4c5300d4 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListView.swift @@ -0,0 +1,24 @@ +import MLSCore +import MLSDesignSystem +import UIKit + +final class BookmarkListView: BaseListView { + let bookmarkEmptyView: DataEmptyView + + init(isFilterHidden: Bool, bookmarkEmptyView: DataEmptyView) { + let editButton = TextButton() + let sortButton = BaseListView.makeSortButton(title: "가나다 순", tintColor: .textColor) + let filterButton = BaseListView.makeFilterButton(title: "필터", tintColor: .textColor) + self.bookmarkEmptyView = bookmarkEmptyView + super.init( + editButton: editButton, + sortButton: sortButton, + filterButton: filterButton, + emptyView: bookmarkEmptyView, + isFilterHidden: isFilterHidden + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListViewController.swift new file mode 100644 index 00000000..31bbdfe3 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListViewController.swift @@ -0,0 +1,363 @@ +import MLSAuthFeatureInterface +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +final class BookmarkListViewController: BaseViewController, View { + typealias Reactor = BookmarkListReactor + + var disposeBag = DisposeBag() + + private let bookmarkChangeRelay = PublishRelay<(id: Int, newBookmarkId: Int?)>() + private let undoRelay = PublishRelay() + + private let itemFilterFactory: ItemFilterBottomSheetFactory + private let monsterFilterFactory: MonsterFilterBottomSheetFactory + private let bookmarkModalFactory: BookmarkModalFactory + private let sortedFactory: SortedBottomSheetFactory + private let loginFactory: LoginFactory + private let dictionaryDetailFactory: DictionaryDetailFactory + private let collectionEditFactory: CollectionEditFactory + + private var selectedSortIndex = 0 + + private var mainView: BookmarkListView + private var emptyView = DataEmptyView(type: .bookmark) + + init( + reactor: BookmarkListReactor, + itemFilterFactory: ItemFilterBottomSheetFactory, + monsterFilterFactory: MonsterFilterBottomSheetFactory, + sortedFactory: SortedBottomSheetFactory, + bookmarkModalFactory: BookmarkModalFactory, + loginFactory: LoginFactory, + dictionaryDetailFactory: DictionaryDetailFactory, + collectionEditFactory: CollectionEditFactory + ) { + self.itemFilterFactory = itemFilterFactory + self.monsterFilterFactory = monsterFilterFactory + self.sortedFactory = sortedFactory + self.bookmarkModalFactory = bookmarkModalFactory + self.loginFactory = loginFactory + self.dictionaryDetailFactory = dictionaryDetailFactory + self.collectionEditFactory = collectionEditFactory + self.mainView = BookmarkListView(isFilterHidden: reactor.currentState.type.isBookmarkSortHidden, bookmarkEmptyView: emptyView) + super.init() + self.reactor = reactor + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + addViews() + setupConstraints() + configureUI() + } +} + +private extension BookmarkListViewController { + func addViews() { + view.addSubview(mainView) + } + + func setupConstraints() { + mainView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.horizontalEdges.bottom.equalToSuperview() + } + } + + func configureUI() { + mainView.listCollectionView.collectionViewLayout = createListLayout() + mainView.listCollectionView.delegate = self + mainView.listCollectionView.dataSource = self + mainView.listCollectionView.register(DictionaryListCell.self, forCellWithReuseIdentifier: DictionaryListCell.identifier) + } + + func createListLayout() -> UICollectionViewLayout { + guard let isHidden = reactor?.currentState.type.isBookmarkSortHidden else { return UICollectionViewLayout() } + let layout = CompositionalLayoutBuilder() + .section { _ in LayoutFactory.getDictionaryListLayout(isFilterHidden: isHidden) } + .build() + layout.register(Neutral300DividerView.self, forDecorationViewOfKind: Neutral300DividerView.identifier) + return layout + } +} + +extension BookmarkListViewController { + func bind(reactor: Reactor) { + // User Actions + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.sortButton.rx.tap + .map { Reactor.Action.sortButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.filterButton.rx.tap + .map { Reactor.Action.filterButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.editButton?.rx.tap + .map { Reactor.Action.editButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + emptyView.button.rx.tap + .map { .emptyButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + // View State + reactor.state + .map(\.items) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, items in + owner.mainView.checkEmptyData(isEmpty: items.isEmpty) + owner.mainView.listCollectionView.reloadData() + }) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, route in + switch route { + case .sort(let type): + let viewController = owner.sortedFactory.make( + sortedOptions: type.bookmarkSortedFilter, + selectedIndex: owner.selectedSortIndex + ) { index in + owner.selectedSortIndex = index + let selectedFilter = reactor.currentState.type.bookmarkSortedFilter[index] + reactor.action.onNext(.sortOptionSelected(selectedFilter)) + owner.mainView.selectSort(selectedType: selectedFilter.rawValue) + } + owner.tabBarController?.presentModal(viewController) + case .filter(let type): + switch type { + case .item: + let viewController = owner.itemFilterFactory.make { results in + reactor.action.onNext(.itemFilterOptionSelected(results)) + if results.isEmpty { owner.mainView.resetFilter() } else { owner.mainView.selectFilter() } + } + owner.present(viewController, animated: true) + case .monster: + let viewController = owner.monsterFilterFactory.make( + startLevel: reactor.currentState.startLevel ?? 1, + endLevel: reactor.currentState.endLevel ?? 200 + ) { startLevel, endLevel in + owner.mainView.selectFilter() + reactor.action.onNext(.filterOptionSelected(startLevel: startLevel, endLevel: endLevel)) + } + owner.tabBarController?.presentModal(viewController) + default: + break + } + case .detail(let type, let id): + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: owner.bookmarkChangeRelay, loginRelay: nil) + owner.navigationController?.pushViewController(viewController, animated: true) + case .login: + let viewController = owner.loginFactory.make(exitRoute: .pop) + owner.navigationController?.pushViewController(viewController, animated: true) + case .dictionary: + if let tabBarController = owner.tabBarController as? BottomTabBarController { + tabBarController.selectTab(index: 0) + DictionaryTabRegistry.changeTab(index: reactor.currentState.type.tabIndex) + } + case .edit: + let viewController = owner.collectionEditFactory.make(bookmarks: reactor.currentState.items) + owner.navigationController?.pushViewController(viewController, animated: true) + case .bookmarkError: + ToastFactory.createToast(message: "북마크 요청에 실패했어요. 다시 시도해주세요.") + default: + break + } + } + .disposed(by: disposeBag) + + reactor.state + .map(\.type) + .distinctUntilChanged() + .withUnretained(self) + .bind(onNext: { owner, type in + owner.mainView.updateBookmarkFilter(type: type.title) + owner.mainView.updateFilter(sortType: type.bookmarkSortedFilter.first?.rawValue) + }) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$uiEvent) } + .withUnretained(self) + .subscribe(onNext: { owner, event in + switch event { + case .add(let item): + owner.presentAddSnackBar(item: item) + case .delete(let item): + owner.presentDeleteSnackBar(item: item) + case .login: + owner.presentLoginGuide() + default: + break + } + }) + .disposed(by: disposeBag) + } + + private func presentAddSnackBar(item: BookmarkResponse) { + let backgroundColor = item.type.backgroundColor + let buttonAction: (() -> Void)? = { [weak self] in + self?.reactor?.state.map(\.items) + .compactMap { items in + items.first(where: { $0.originalId == item.originalId })?.bookmarkId + } + .take(1) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] bookmarkId in + guard let self else { return } + let vc = self.bookmarkModalFactory.make(bookmarkIds: [bookmarkId]) { isAdd in + if isAdd { + ToastFactory.createToast(message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요.") + } + } + vc.modalPresentationStyle = .pageSheet + if let sheet = vc.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 16 + } + self.present(vc, animated: true) + }) + .disposed(by: self?.disposeBag ?? DisposeBag()) + } + if let urlString = item.imageUrl, let url = URL(string: urlString) { + ImageLoader.shared.loadImage(url: url) { image in + DispatchQueue.main.async { + SnackBarFactory.createSnackBar( + type: .normal, + image: image ?? UIImage(), + imageBackgroundColor: backgroundColor, + text: "아이템을 북마크에 추가했어요.", + buttonText: "컬렉션 추가", + buttonAction: buttonAction + ) + } + } + } else { + SnackBarFactory.createSnackBar( + type: .normal, + image: UIImage(), + imageBackgroundColor: backgroundColor, + text: "아이템을 북마크에 추가했어요.", + buttonText: "컬렉션 추가", + buttonAction: buttonAction + ) + } + } + + private func presentDeleteSnackBar(item: BookmarkResponse) { + let backgroundColor = item.type.backgroundColor + let buttonAction: (() -> Void)? = { [weak self] in + self?.undoRelay.accept(()) + self?.reactor?.action.onNext(.undoLastDeletedBookmark) + } + if let urlString = item.imageUrl, let url = URL(string: urlString) { + ImageLoader.shared.loadImage(url: url) { image in + DispatchQueue.main.async { + SnackBarFactory.createSnackBar( + type: .delete, + image: image ?? UIImage(), + imageBackgroundColor: backgroundColor, + text: "아이템을 북마크에서 삭제했어요.", + buttonText: "되돌리기", + buttonAction: buttonAction + ) + } + } + } else { + SnackBarFactory.createSnackBar( + type: .delete, + image: UIImage(), + imageBackgroundColor: backgroundColor, + text: "아이템을 북마크에서 삭제했어요.", + buttonText: "되돌리기", + buttonAction: buttonAction + ) + } + } + + private func presentLoginGuide() { + GuideAlertFactory.show( + mainText: "북마크를 하려면 로그인이 필요해요.", + ctaText: "로그인 하기", + cancelText: "취소", + ctaAction: { [weak self] in + guard let self else { return } + let vc = self.loginFactory.make(exitRoute: .pop) + self.navigationController?.pushViewController(vc, animated: true) + }, + cancelAction: nil + ) + } +} + +extension BookmarkListViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + reactor?.currentState.items.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let state = reactor?.currentState else { return UICollectionViewCell() } + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DictionaryListCell.identifier, for: indexPath) as? DictionaryListCell else { + return UICollectionViewCell() + } + let item = state.items[indexPath.row] + var subText: String? { + [.item, .monster, .quest].contains(item.type) ? item.level.map { "Lv. \($0)" } : nil + } + cell.inject( + type: .bookmark, + input: DictionaryListCell.Input( + type: item.type, + mainText: item.name, + subText: subText, + imageUrl: item.imageUrl ?? "", + isBookmarked: true + ), + indexPath: indexPath, + collectionView: collectionView, + isMap: item.type == .map, + onBookmarkTapped: { [weak self] in + guard let self else { return } + guard self.reactor?.currentState.isLogin == true else { + self.reactor?.action.onNext(.showLogin) + return + } + self.reactor?.action.onNext(.toggleBookmark(item.originalId)) + } + ) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + reactor?.action.onNext(.dataTapped(indexPath.item)) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainFactoryImpl.swift new file mode 100644 index 00000000..fe9cd5d0 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainFactoryImpl.swift @@ -0,0 +1,52 @@ +import MLSAuthFeatureInterface +import MLSBookmarkFeatureInterface +import MLSCore +import UIKit + +public final class BookmarkMainFactoryImpl: BookmarkMainFactory { + private let authRepository: BookmarkAuthRepository + private let userDefaultsRepository: BookmarkUserDefaultsRepository + private let onBoardingFactory: BookmarkOnBoardingFactory + private let bookmarkListFactory: BookmarkListFactory + private let collectionListFactory: CollectionListFactory + private let searchFactory: DictionarySearchFactory + private let notificationFactory: DictionaryNotificationFactory + private let loginFactory: LoginFactory + + public init( + authRepository: BookmarkAuthRepository, + userDefaultsRepository: BookmarkUserDefaultsRepository, + onBoardingFactory: BookmarkOnBoardingFactory, + bookmarkListFactory: BookmarkListFactory, + collectionListFactory: CollectionListFactory, + searchFactory: DictionarySearchFactory, + notificationFactory: DictionaryNotificationFactory, + loginFactory: LoginFactory + ) { + self.authRepository = authRepository + self.userDefaultsRepository = userDefaultsRepository + self.onBoardingFactory = onBoardingFactory + self.bookmarkListFactory = bookmarkListFactory + self.collectionListFactory = collectionListFactory + self.searchFactory = searchFactory + self.notificationFactory = notificationFactory + self.loginFactory = loginFactory + } + + public func make(bottomInset: CGFloat = 64) -> BaseViewController { + let reactor = BookmarkMainReactor( + authRepository: authRepository, + userDefaultsRepository: userDefaultsRepository + ) + return BookmarkMainViewController( + bottomInset: bottomInset, + onBoardingFactory: onBoardingFactory, + bookmarkListFactory: bookmarkListFactory, + collectionListFactory: collectionListFactory, + searchFactory: searchFactory, + notificationFactory: notificationFactory, + loginFactory: loginFactory, + reactor: reactor + ) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainReactor.swift new file mode 100644 index 00000000..404ab2c3 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainReactor.swift @@ -0,0 +1,75 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class BookmarkMainReactor: Reactor { + enum Route { + case none + case search + case onBoarding + case notification + case edit + case login + } + + enum Action { + case viewWillAppear + case searchButtonTapped + case notificationButtonTapped + case loginButtonTapped + } + + enum Mutation { + case navigateTo(Route) + case setLogin(Bool) + } + + struct State { + @Pulse var route: Route + let type = DictionaryMainViewType.bookmark + var sections: [String] { + return type.pageTabList.map { $0.title } + } + var isLogin = false + } + + var initialState: State + + private let authRepository: BookmarkAuthRepository + private let userDefaultsRepository: BookmarkUserDefaultsRepository + + init(authRepository: BookmarkAuthRepository, userDefaultsRepository: BookmarkUserDefaultsRepository) { + self.initialState = State(route: .none) + self.authRepository = authRepository + self.userDefaultsRepository = userDefaultsRepository + } + + func mutate(action: Action) -> Observable { + switch action { + case .viewWillAppear: + let onboardingMutation = userDefaultsRepository.hasVisitedOnboarding() + .flatMap { hasVisited -> Observable in + if hasVisited { return .empty() } else { return .just(.navigateTo(.onBoarding)) } + } + let loginMutation = authRepository.isLoggedIn().map { Mutation.setLogin($0) } + return .concat([onboardingMutation, loginMutation]) + case .searchButtonTapped: + return .just(.navigateTo(.search)) + case .notificationButtonTapped: + return .just(.navigateTo(.notification)) + case .loginButtonTapped: + return .just(.navigateTo(.login)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .navigateTo(route): + newState.route = route + case let .setLogin(isLogin): + newState.isLogin = isLogin + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainView.swift new file mode 100644 index 00000000..5dc3caa5 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainView.swift @@ -0,0 +1,85 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import SnapKit +import UIKit + +final class BookmarkMainView: UIView { + enum Constant { + static let topMargin: CGFloat = 20 + static let pageTabHeight: CGFloat = 40 + static let bottomTabHeight: CGFloat = 64 + } + + let headerView = Header(style: .main, title: "북마크") + + let tabCollectionView: UICollectionView = { + let layout = UICollectionViewLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = false + return collectionView + }() + + let pageViewController = UIPageViewController( + transitionStyle: .scroll, + navigationOrientation: .horizontal + ) + + let emptyView = ToLoginView(type: .bookmark) + + init(type: DictionaryMainViewType, bottomInset: CGFloat = Constant.bottomTabHeight) { + super.init(frame: .zero) + setupBaseLayout(type: type, bottomInset: bottomInset) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension BookmarkMainView { + func setupBaseLayout(type: DictionaryMainViewType, bottomInset: CGFloat) { + addSubview(headerView) + headerView.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide) + make.horizontalEdges.equalToSuperview() + } + + addSubview(tabCollectionView) + addSubview(pageViewController.view) + addSubview(emptyView) + + tabCollectionView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.equalToSuperview() + make.height.equalTo(Constant.pageTabHeight) + } + + pageViewController.view.snp.makeConstraints { make in + make.top.equalTo(tabCollectionView.snp.bottom) + make.horizontalEdges.equalTo(safeAreaLayoutGuide) + make.bottom.equalToSuperview().inset(bottomInset) + } + + emptyView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview().inset(bottomInset) + } + + tabCollectionView.isHidden = true + pageViewController.view.isHidden = true + emptyView.isHidden = false + } +} + +extension BookmarkMainView { + func updateLoginState(isLogin: Bool) { + tabCollectionView.isHidden = !isLogin + pageViewController.view.isHidden = !isLogin + emptyView.isHidden = isLogin + tabCollectionView.isUserInteractionEnabled = isLogin + pageViewController.view.isUserInteractionEnabled = isLogin + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainViewController.swift new file mode 100644 index 00000000..71f780e1 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkMain/BookmarkMainViewController.swift @@ -0,0 +1,240 @@ +import MLSAuthFeatureInterface +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit +import UIKit + +final class BookmarkMainViewController: BaseViewController, View { + typealias Reactor = BookmarkMainReactor + + var disposeBag = DisposeBag() + + private let initialIndex: Int + private lazy var currentPageIndex = BehaviorRelay(value: initialIndex) + + private let onBoardingFactory: BookmarkOnBoardingFactory + private let searchFactory: DictionarySearchFactory + private let notificationFactory: DictionaryNotificationFactory + private let loginFactory: LoginFactory + + private var viewControllers: [UIViewController] + private let mainView: BookmarkMainView + private let underLineController = TabBarUnderlineController() + + init( + initialIndex: Int = 0, + bottomInset: CGFloat = BookmarkMainView.Constant.bottomTabHeight, + onBoardingFactory: BookmarkOnBoardingFactory, + bookmarkListFactory: BookmarkListFactory, + collectionListFactory: CollectionListFactory, + searchFactory: DictionarySearchFactory, + notificationFactory: DictionaryNotificationFactory, + loginFactory: LoginFactory, + reactor: BookmarkMainReactor + ) { + let type = reactor.currentState.type + self.mainView = BookmarkMainView(type: type, bottomInset: bottomInset) + self.viewControllers = type.pageTabList.enumerated().map { index, tabType in + if index == 1 { + return collectionListFactory.make() + } else { + return bookmarkListFactory.make(type: tabType, listType: type) + } + } + self.onBoardingFactory = onBoardingFactory + self.searchFactory = searchFactory + self.notificationFactory = notificationFactory + self.loginFactory = loginFactory + self.initialIndex = initialIndex + super.init() + self.reactor = reactor + } + + @available(*, unavailable) + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Life Cycle +extension BookmarkMainViewController { + override func viewDidLoad() { + super.viewDidLoad() + addViews() + setupConstraints() + configureUI() + setInitialIndex() + } +} + +// MARK: - SetUp +private extension BookmarkMainViewController { + func addViews() { + addChild(mainView.pageViewController) + mainView.pageViewController.didMove(toParent: self) + view.addSubview(mainView) + } + + func setupConstraints() { + mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configureUI() { + mainView.pageViewController.delegate = self + mainView.pageViewController.dataSource = self + configureTabCollectionView() + } + + func configureTabCollectionView() { + mainView.tabCollectionView.collectionViewLayout = createTabLayout() + mainView.tabCollectionView.delegate = self + mainView.tabCollectionView.dataSource = self + mainView.tabCollectionView.register(PageTabbarCell.self, forCellWithReuseIdentifier: PageTabbarCell.identifier) + underLineController.configure(with: mainView.tabCollectionView) + } + + func createTabLayout() -> UICollectionViewLayout { + let layout = CompositionalLayoutBuilder() + .section { _ in LayoutFactory.getPageTabbarLayout(underLineController: underLineController) } + .build() + return layout + } + + func setInitialIndex() { + let indexPath = IndexPath(item: initialIndex, section: 0) + mainView.pageViewController.setViewControllers( + [viewControllers[initialIndex]], + direction: .forward, + animated: false, + completion: nil + ) + mainView.tabCollectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredHorizontally) + DispatchQueue.main.async { [weak self] in + self?.underLineController.setInitialIndicator() + } + } +} + +// MARK: - Bind +extension BookmarkMainViewController { + func bind(reactor: Reactor) { + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.headerView.firstIconButton.rx.tap + .map { Reactor.Action.searchButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.headerView.secondIconButton.rx.tap + .map { Reactor.Action.notificationButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.emptyView.button.rx.tap + .map { Reactor.Action.loginButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .subscribe { owner, route in + switch route { + case .search: + let controller = owner.searchFactory.make() + owner.navigationController?.pushViewController(controller, animated: true) + case .notification: + let controller = owner.notificationFactory.make() + owner.navigationController?.pushViewController(controller, animated: true) + case .onBoarding: + let viewController = owner.onBoardingFactory.make() + viewController.modalPresentationStyle = .fullScreen + owner.present(viewController, animated: true) + case .login: + let controller = owner.loginFactory.make(exitRoute: .pop, onLoginCompleted: nil) + owner.navigationController?.pushViewController(controller, animated: true) + default: + break + } + } + .disposed(by: disposeBag) + + reactor.state + .map { $0.isLogin } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind { owner, isLogin in + owner.mainView.updateLoginState(isLogin: isLogin) + owner.underLineController.setHidden(hidden: !isLogin) + } + .disposed(by: disposeBag) + } +} + +// MARK: - UIPageViewController DataSource & Delegate +extension BookmarkMainViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate { + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.firstIndex(of: viewController) else { return nil } + let previousIndex = index - 1 + return previousIndex >= 0 ? viewControllers[previousIndex] : nil + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.firstIndex(of: viewController) else { return nil } + let nextIndex = index + 1 + return nextIndex < viewControllers.count ? viewControllers[nextIndex] : nil + } + + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + if completed, let visibleViewController = pageViewController.viewControllers?.first, + let newIndex = viewControllers.firstIndex(of: visibleViewController) { + currentPageIndex.accept(newIndex) + mainView.tabCollectionView.selectItem(at: IndexPath(item: newIndex, section: 0), animated: true, scrollPosition: .centeredHorizontally) + underLineController.animateIndicatorToSelectedItem() + } + } +} + +// MARK: - UICollectionView DataSource & Delegate +extension BookmarkMainViewController: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + guard let reactor = reactor else { return 0 } + return reactor.currentState.sections.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let reactor = reactor else { return UICollectionViewCell() } + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PageTabbarCell.identifier, for: indexPath) as? PageTabbarCell else { + return UICollectionViewCell() + } + let title = reactor.currentState.sections[indexPath.row] + cell.inject(title: title) + cell.isSelected = indexPath.row == currentPageIndex.value + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let newIndex = indexPath.row + let oldIndex = currentPageIndex.value + guard newIndex != oldIndex else { return } + let direction: UIPageViewController.NavigationDirection = newIndex > oldIndex ? .forward : .reverse + mainView.pageViewController.setViewControllers( + [viewControllers[newIndex]], + direction: direction, + animated: true, + completion: nil + ) + currentPageIndex.accept(newIndex) + underLineController.animateIndicatorToSelectedItem() + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalFactoryImpl.swift new file mode 100644 index 00000000..fdc51e48 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalFactoryImpl.swift @@ -0,0 +1,27 @@ +import MLSBookmarkFeatureInterface +import MLSCore + +public final class BookmarkModalFactoryImpl: BookmarkModalFactory { + private let addCollectionFactory: AddCollectionFactory + private let collectionRepository: CollectionRepository + + public init(addCollectionFactory: AddCollectionFactory, collectionRepository: CollectionRepository) { + self.addCollectionFactory = addCollectionFactory + self.collectionRepository = collectionRepository + } + + public func make(bookmarkIds: [Int]) -> BaseViewController { + let reactor = BookmarkModalReactor(bookmarkIds: bookmarkIds, collectionRepository: collectionRepository) + let viewController = BookmarkModalViewController(addCollectionFactory: addCollectionFactory) + viewController.reactor = reactor + return viewController + } + + public func make(bookmarkIds: [Int], onComplete: ((Bool) -> Void)?) -> BaseViewController { + let viewController = make(bookmarkIds: bookmarkIds) + if let vc = viewController as? BookmarkModalViewController { + vc.onCompleted = onComplete + } + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalReactor.swift new file mode 100644 index 00000000..7e3d6ccb --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalReactor.swift @@ -0,0 +1,88 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class BookmarkModalReactor: Reactor { + enum Route { + case none + case dismiss + case dismissWithData + case addCollection + case collectionError + } + + enum Action { + case backButtonTapped + case addButtonTapped + case completeAdding + case addCollectionTapped + case selectItem(Int) + case viewWillAppear + } + + enum Mutation { + case navigateTo(Route) + case checkCollection([CollectionResponse]) + case setCollection([CollectionResponse]) + } + + struct State { + @Pulse var route: Route + var bookmarkIds: [Int] + var collections = [CollectionResponse]() + var selectedItems = [CollectionResponse]() + } + + var initialState: State + + private let collectionRepository: CollectionRepository + + init(bookmarkIds: [Int], collectionRepository: CollectionRepository) { + self.initialState = State(route: .none, bookmarkIds: bookmarkIds) + self.collectionRepository = collectionRepository + } + + func mutate(action: Action) -> Observable { + switch action { + case .viewWillAppear, .completeAdding: + return collectionRepository.fetchCollectionList(sort: nil) + .map { .setCollection($0) } + + case .addButtonTapped: + return collectionRepository.addCollectionAndBookmark( + collectionIds: currentState.selectedItems.map { $0.collectionId }, + bookmarkIds: currentState.bookmarkIds + ) + .andThen(.just(.navigateTo(.dismissWithData))) + .catch { _ in .just(.navigateTo(.collectionError)) } + + case .backButtonTapped: + return .just(.navigateTo(.dismiss)) + + case .addCollectionTapped: + return .just(.navigateTo(.addCollection)) + + case .selectItem(let id): + var newItems = currentState.selectedItems + if let index = newItems.firstIndex(where: { $0.collectionId == id }) { + newItems.remove(at: index) + } else if let collection = currentState.collections.first(where: { $0.collectionId == id }) { + newItems.append(collection) + } + return .just(.checkCollection(newItems)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .navigateTo(let route): + newState.route = route + case .checkCollection(let collections): + newState.selectedItems = collections + case .setCollection(let collections): + newState.collections = collections + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalView.swift new file mode 100644 index 00000000..8e05072c --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalView.swift @@ -0,0 +1,91 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class BookmarkModalView: UIView { + private enum Constant { + static let buttonTopMargin: CGFloat = 12 + static let buttonBottomMargin: CGFloat = 14 + static let horizontalMargin: CGFloat = 16 + } + + private let titleLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .h_xl_b, text: "컬렉션", alignment: .left) + return label + }() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(DesignSystemAsset.image(named: "largeX"), for: .normal) + return button + }() + + let folderCollectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + return collectionView + }() + + private let divider = DividerView() + + let addButtton = CommonButton(style: .normal, title: "", disabledTitle: "추가하기") + + init() { + super.init(frame: .zero) + addViews() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +private extension BookmarkModalView { + func addViews() { + addSubview(titleLabel) + addSubview(backButton) + addSubview(folderCollectionView) + addSubview(divider) + addSubview(addButtton) + } + + func setupConstraints() { + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(40) + make.leading.equalToSuperview().inset(Constant.horizontalMargin) + } + + backButton.snp.makeConstraints { make in + make.top.equalTo(titleLabel) + make.leading.equalTo(titleLabel.snp.trailing) + make.trailing.equalToSuperview().inset(Constant.horizontalMargin) + } + + folderCollectionView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(12) + make.horizontalEdges.equalToSuperview() + } + + divider.snp.makeConstraints { make in + make.top.equalTo(folderCollectionView.snp.bottom) + make.horizontalEdges.equalToSuperview() + } + + addButtton.snp.makeConstraints { make in + make.top.equalTo(folderCollectionView.snp.bottom).offset(Constant.buttonTopMargin) + make.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + make.bottom.equalToSuperview().inset(Constant.buttonBottomMargin) + } + } +} + +extension BookmarkModalView { + func setButtonTitle(count: Int) { + if count == 0 { + addButtton.isEnabled = false + } else { + addButtton.isEnabled = true + addButtton.updateTitle(title: "\(count)개의 컬렉션 추가하기") + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalViewController.swift new file mode 100644 index 00000000..879688be --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkModal/BookmarkModalViewController.swift @@ -0,0 +1,157 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit +import UIKit + +final class BookmarkModalViewController: BaseViewController, View { + typealias Reactor = BookmarkModalReactor + + var disposeBag = DisposeBag() + var onCompleted: ((Bool) -> Void)? + + private let addCollectionFactory: AddCollectionFactory + private let mainView = BookmarkModalView() + + init(addCollectionFactory: AddCollectionFactory) { + self.addCollectionFactory = addCollectionFactory + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + addViews() + setupConstraints() + configureUI() + } +} + +private extension BookmarkModalViewController { + func addViews() { + view.addSubview(mainView) + } + + func setupConstraints() { + mainView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + } + + func configureUI() { + mainView.folderCollectionView.collectionViewLayout = createListLayout() + mainView.folderCollectionView.delegate = self + mainView.folderCollectionView.dataSource = self + mainView.folderCollectionView.register(AddFolderCell.self, forCellWithReuseIdentifier: AddFolderCell.identifier) + mainView.folderCollectionView.register(FolderCell.self, forCellWithReuseIdentifier: FolderCell.identifier) + } + + func createListLayout() -> UICollectionViewLayout { + CompositionalLayoutBuilder() + .section { _ in LayoutFactory.getCollectionModalLayout() } + .build() + } +} + +extension BookmarkModalViewController { + func bind(reactor: Reactor) { + rx.viewWillAppear + .map { .viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.backButton.rx.tap + .map { Reactor.Action.backButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.addButtton.rx.tap + .map { Reactor.Action.addButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .map(\.collections) + .observe(on: MainScheduler.instance) + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.mainView.folderCollectionView.reloadData() + }) + .disposed(by: disposeBag) + + reactor.state + .map(\.selectedItems) + .observe(on: MainScheduler.instance) + .withUnretained(self) + .bind(onNext: { owner, items in + owner.mainView.setButtonTitle(count: items.count) + }) + .disposed(by: disposeBag) + + rx.viewDidAppear + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { owner, route in + switch route { + case .dismissWithData: + owner.onCompleted?(true) + owner.dismiss(animated: true) + case .dismiss: + owner.onCompleted?(false) + owner.dismiss(animated: true) + case .addCollection: + let viewController = owner.addCollectionFactory.make(collection: nil) + guard let vc = viewController as? AddCollectionViewController else { return } + vc.dismissed + .withUnretained(owner) + .subscribe { o, _ in + o.reactor?.action.onNext(.completeAdding) + } + .disposed(by: owner.disposeBag) + owner.present(vc, animated: true) + case .collectionError: + ToastFactory.createToast(message: "컬렉션 저장에 실패했어요. 다시 시도해주세요.") + default: + break + } + }) + .disposed(by: disposeBag) + } +} + +extension BookmarkModalViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + (reactor?.currentState.collections.count ?? 0) + 1 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + if indexPath.row == 0 { + return collectionView.dequeueReusableCell(withReuseIdentifier: AddFolderCell.identifier, for: indexPath) as? AddFolderCell ?? UICollectionViewCell() + } + guard let reactor, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FolderCell.identifier, for: indexPath) as? FolderCell else { + return UICollectionViewCell() + } + let collection = reactor.currentState.collections[indexPath.row - 1] + let isSelected = reactor.currentState.selectedItems.contains(where: { $0.collectionId == collection.collectionId }) + cell.isChecked = isSelected + cell.inject(title: collection.name) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.row == 0 { + reactor?.action.onNext(.addCollectionTapped) + } else { + guard let collection = reactor?.currentState.collections[indexPath.row - 1] else { return } + reactor?.action.onNext(.selectItem(collection.collectionId)) + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingFactoryImpl.swift new file mode 100644 index 00000000..17c87836 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingFactoryImpl.swift @@ -0,0 +1,13 @@ +import MLSBookmarkFeatureInterface +import MLSCore + +public final class BookmarkOnBoardingFactoryImpl: BookmarkOnBoardingFactory { + public init() {} + + public func make() -> BaseViewController { + let reactor = BookmarkOnBoardingReactor() + let viewController = BookmarkOnBoardingViewController() + viewController.reactor = reactor + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingReactor.swift new file mode 100644 index 00000000..175172e0 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingReactor.swift @@ -0,0 +1,55 @@ +import ReactorKit +import RxSwift + +final class BookmarkOnBoardingReactor: Reactor { + enum Route { + case none + case dismiss + } + + enum Action { + case nextButtonTapped + case backButtonTapped + } + + enum Mutation { + case setStep(BookmarkOnBoardingView.OnBoardingIndexType) + case dismiss + } + + struct State { + @Pulse var route: Route = .none + var step: BookmarkOnBoardingView.OnBoardingIndexType = .first + } + + var initialState: State + + init() { + self.initialState = State() + } + + func mutate(action: Action) -> Observable { + switch action { + case .nextButtonTapped: + let next = currentState.step.next() + if next == .end { + return .just(.dismiss) + } else { + return .just(.setStep(next)) + } + case .backButtonTapped: + return .just(.setStep(currentState.step.previous())) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setStep(let step): + newState.step = step + case .dismiss: + newState.route = .dismiss + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingView.swift new file mode 100644 index 00000000..a2bb11fe --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingView.swift @@ -0,0 +1,149 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class BookmarkOnBoardingView: UIView { + enum OnBoardingIndexType: Int { + case first = 0 + case second + case end + + struct Content { + let imageName: String + let title: String + let description: String + let isBackButtonHidden: Bool + let buttonTitle: String + } + + var content: Content? { + switch self { + case .first: + return .init( + imageName: "onBoardingBookmark", + title: "내가 찜한 정보, 한곳에!", + description: "아이템, 몬스터, 맵, NPC, 퀘스트를\n북마크하면 자동으로 여기에 모여요.", + isBackButtonHidden: true, + buttonTitle: "다음" + ) + case .second: + return .init( + imageName: "addToCollection", + title: "나만의 컬렉션 만들기", + description: "북마크한 정보들을 폴더로 정리해보세요.", + isBackButtonHidden: false, + buttonTitle: "북마크 열기" + ) + default: + return nil + } + } + + func next() -> Self { + switch self { + case .first: return .second + case .second: return .end + case .end: return .end + } + } + + func previous() -> Self { + switch self { + case .first: return .first + case .second: return .first + case .end: return .second + } + } + } + + private enum Constant { + static let imageSize: CGFloat = 220 + static let imageYOffset: CGFloat = -116 + static let imageSpacing: CGFloat = 4 + static let labelSpacing: CGFloat = 16 + static let indicatorSpacing: CGFloat = 29 + static let bottomInset: CGFloat = 16 + static let horizontalMargin: CGFloat = 16 + static let backButtonTrailing: CGFloat = -28 + static let backButtonBottom: CGFloat = 10 + } + + private let imageView = UIImageView() + private let titleLabel = UILabel() + private let descLabel = UILabel() + + let backButton: UIButton = { + let button = UIButton() + button.setImage(DesignSystemAsset.image(named: "arrowBack"), for: .normal) + return button + }() + + private let stepIndicator = StepIndicator(circleCount: 2) + let nextButton = CommonButton(style: .normal, title: "다음", disabledTitle: nil) + + init() { + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI(type: .first) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +private extension BookmarkOnBoardingView { + func addViews() { + addSubview(imageView) + addSubview(titleLabel) + addSubview(descLabel) + addSubview(backButton) + addSubview(stepIndicator) + addSubview(nextButton) + } + + func setupConstraints() { + imageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview().offset(Constant.imageYOffset) + make.size.equalTo(Constant.imageSize) + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom).offset(Constant.imageSpacing) + make.centerX.equalToSuperview() + } + + descLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(Constant.labelSpacing) + make.centerX.equalToSuperview() + } + + backButton.snp.makeConstraints { make in + make.trailing.equalTo(imageView.snp.leading).inset(Constant.backButtonTrailing) + make.bottom.equalTo(imageView.snp.bottom).inset(Constant.backButtonBottom) + } + + stepIndicator.snp.makeConstraints { make in + make.top.equalTo(descLabel.snp.bottom).offset(Constant.indicatorSpacing) + make.centerX.equalToSuperview() + } + + nextButton.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + make.bottom.equalToSuperview().inset(Constant.bottomInset) + } + } +} + +extension BookmarkOnBoardingView { + func configureUI(type: OnBoardingIndexType) { + guard let content = type.content else { return } + imageView.image = DesignSystemAsset.image(named: content.imageName) + titleLabel.attributedText = .makeStyledString(font: .h_xxxl_b, text: content.title) + descLabel.attributedText = .makeStyledString(font: .b_m_r, text: content.description, color: .neutral700) + nextButton.updateTitle(title: content.buttonTitle) + backButton.isHidden = content.isBackButtonHidden + stepIndicator.selectIndicator(index: type.rawValue) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingViewController.swift new file mode 100644 index 00000000..0009f6d3 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkOnBoarding/BookmarkOnBoardingViewController.swift @@ -0,0 +1,64 @@ +import MLSCore +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit +import UIKit + +final class BookmarkOnBoardingViewController: BaseViewController, View { + typealias Reactor = BookmarkOnBoardingReactor + + var disposeBag = DisposeBag() + private let mainView = BookmarkOnBoardingView() + + override init() { + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(mainView) + mainView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + } + + func bind(reactor: Reactor) { + mainView.backButton.rx.tap + .map { Reactor.Action.backButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.nextButton.rx.tap + .map { Reactor.Action.nextButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .subscribe(onNext: { owner, route in + switch route { + case .dismiss: + owner.dismiss(animated: true) + default: + break + } + }) + .disposed(by: disposeBag) + + reactor.state + .map(\.step) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, step in + owner.mainView.configureUI(type: step) + }) + .disposed(by: disposeBag) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailEmptyView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailEmptyView.swift new file mode 100644 index 00000000..f6388220 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailEmptyView.swift @@ -0,0 +1,66 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionDetailEmptyView: UIView { + private enum Constant { + static let imageSize: CGFloat = 220 + static let textSpacing: CGFloat = 10 + static let buttonSpacing: CGFloat = 24 + static let buttonWidth: CGFloat = 186 + } + + let imageView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "connectionError") + return view + }() + + private let mainLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .h_xl_b, text: "아직 아무것도 없어요!", color: .textColor) + return label + }() + + private let subLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .b_s_r, text: "북마크해서 추가해보세요.", color: .neutral600) + return label + }() + + let bookmarkButton = CommonButton(style: .normal, title: "북마크하러 가기", disabledTitle: nil) + + init() { + super.init(frame: .zero) + addSubview(imageView) + addSubview(mainLabel) + addSubview(subLabel) + addSubview(bookmarkButton) + + imageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.size.equalTo(Constant.imageSize) + } + + mainLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom) + make.center.equalToSuperview() + } + + subLabel.snp.makeConstraints { make in + make.top.equalTo(mainLabel.snp.bottom).offset(Constant.textSpacing) + make.centerX.equalToSuperview() + } + + bookmarkButton.snp.makeConstraints { make in + make.top.equalTo(subLabel.snp.bottom).offset(Constant.buttonSpacing) + make.width.equalTo(Constant.buttonWidth) + make.centerX.equalToSuperview() + } + + backgroundColor = .clearMLS + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailFactoryImpl.swift new file mode 100644 index 00000000..63a2effa --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailFactoryImpl.swift @@ -0,0 +1,59 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import RxSwift + +public final class CollectionDetailFactoryImpl: CollectionDetailFactory { + private let bookmarkModalFactory: BookmarkModalFactory + private let collectionSettingFactory: CollectionSettingFactory + private let addCollectionFactory: AddCollectionFactory + private let collectionEditFactory: CollectionEditFactory + private let dictionaryDetailFactory: DictionaryDetailFactory + private let collectionRepository: CollectionRepository + + public init( + bookmarkModalFactory: BookmarkModalFactory, + collectionSettingFactory: CollectionSettingFactory, + addCollectionFactory: AddCollectionFactory, + collectionEditFactory: CollectionEditFactory, + dictionaryDetailFactory: DictionaryDetailFactory, + collectionRepository: CollectionRepository + ) { + self.bookmarkModalFactory = bookmarkModalFactory + self.collectionSettingFactory = collectionSettingFactory + self.addCollectionFactory = addCollectionFactory + self.collectionEditFactory = collectionEditFactory + self.dictionaryDetailFactory = dictionaryDetailFactory + self.collectionRepository = collectionRepository + } + + public func make(collection: CollectionResponse, onMoveToMain: (() -> Void)?) -> BaseViewController { + let reactor = CollectionDetailReactor( + collection: collection, + collectionRepository: collectionRepository + ) + let viewController = CollectionDetailViewController( + reactor: reactor, + bookmarkModalFactory: bookmarkModalFactory, + collectionSettingFactory: collectionSettingFactory, + addCollectionFactory: addCollectionFactory, + collectionEditFactory: collectionEditFactory, + dictionaryDetailFactory: dictionaryDetailFactory + ) + + reactor.pulse(\.$route) + .observe(on: MainScheduler.instance) + .bind(onNext: { [weak viewController] route in + switch route { + case .toMain: + onMoveToMain?() + viewController?.navigationController?.popToRootViewController(animated: true) + default: + break + } + }) + .disposed(by: viewController.disposeBag) + + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailReactor.swift new file mode 100644 index 00000000..9534d99e --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailReactor.swift @@ -0,0 +1,86 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class CollectionDetailReactor: Reactor { + enum Route { + case none + case toMain + case dismiss + case edit + case detail(DictionaryType, Int) + } + + enum Action { + case viewWillAppear + case backButtonTapped + case editButtonTapped + case addButtonTapped + case bookmarkButtonTapped + case selectSetting(CollectionSettingMenu) + case changeName(String) + case dataTapped(Int) + case deleteCollection + } + + enum Mutation { + case navigateTo(Route) + case setItems([BookmarkResponse]) + case setMenu(CollectionSettingMenu) + case setName(String) + } + + struct State { + @Pulse var route: Route + @Pulse var collectionMenu: CollectionSettingMenu? + var collection: CollectionResponse + } + + var initialState: State + private let collectionRepository: CollectionRepository + + init(collection: CollectionResponse, collectionRepository: CollectionRepository) { + self.initialState = State(route: .none, collection: collection) + self.collectionRepository = collectionRepository + } + + func mutate(action: Action) -> Observable { + switch action { + case .backButtonTapped: + return .just(.navigateTo(.dismiss)) + case .editButtonTapped: + return .just(.navigateTo(.edit)) + case .viewWillAppear: + return collectionRepository.fetchCollection(id: currentState.collection.collectionId) + .map { .setItems($0) } + case .addButtonTapped, .bookmarkButtonTapped: + return .just(.navigateTo(.toMain)) + case .selectSetting(let menu): + return .just(.setMenu(menu)) + case .changeName(let name): + return .just(.setName(name)) + case .dataTapped(let index): + let item = currentState.collection.recentBookmarks[index] + guard let type = item.type.toDictionaryType else { return .empty() } + return .just(.navigateTo(.detail(type, item.originalId))) + case .deleteCollection: + return collectionRepository.deleteCollection(collectionId: currentState.collection.collectionId) + .andThen(.just(.navigateTo(.dismiss))) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setItems(let items): + newState.collection.recentBookmarks = items + case .navigateTo(let route): + newState.route = route + case .setMenu(let menu): + newState.collectionMenu = menu + case .setName(let name): + newState.collection.name = name + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailView.swift new file mode 100644 index 00000000..b4b6a29b --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailView.swift @@ -0,0 +1,87 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionDetailView: UIView { + private enum Constant { + static let topMargin: CGFloat = 12 + static let collectionViewMargin: CGFloat = 24 + } + + let navigation: NavigationBar + let spacer: UIView = { + let view = UIView() + view.backgroundColor = .whiteMLS + return view + }() + let listCollectionView: UICollectionView = { + UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + }() + let emptyContainerView = UIView() + let emptyView = CollectionDetailEmptyView() + + init(navTitle: String) { + self.navigation = NavigationBar(type: .collection(navTitle)) + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +private extension CollectionDetailView { + func addViews() { + addSubview(navigation) + addSubview(spacer) + addSubview(listCollectionView) + addSubview(emptyContainerView) + emptyContainerView.addSubview(emptyView) + } + + func setupConstraints() { + navigation.snp.makeConstraints { make in + make.top.horizontalEdges.equalTo(safeAreaLayoutGuide) + } + + spacer.snp.makeConstraints { make in + make.top.equalTo(navigation.snp.bottom) + make.horizontalEdges.equalToSuperview() + make.height.equalTo(Constant.topMargin) + } + + listCollectionView.snp.makeConstraints { make in + make.top.equalTo(spacer.snp.bottom).offset(Constant.collectionViewMargin) + make.horizontalEdges.bottom.equalToSuperview() + } + + emptyContainerView.snp.makeConstraints { make in + make.top.equalTo(navigation.snp.bottom).offset(Constant.collectionViewMargin) + make.horizontalEdges.bottom.equalTo(safeAreaLayoutGuide) + } + + emptyView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configureUI() { + navigation.backgroundColor = .whiteMLS + backgroundColor = .neutral100 + emptyContainerView.backgroundColor = .neutral100 + listCollectionView.backgroundColor = .neutral100 + } +} + +extension CollectionDetailView { + func isEmptyData(isEmpty: Bool) { + listCollectionView.isHidden = isEmpty + emptyContainerView.isHidden = !isEmpty + } + + func setName(name: String) { + navigation.setTitle(title: name) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailViewController.swift new file mode 100644 index 00000000..1980ab53 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionDetail/CollectionDetailViewController.swift @@ -0,0 +1,209 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxCocoa +import RxSwift +import UIKit + +final class CollectionDetailViewController: BaseViewController, View { + typealias Reactor = CollectionDetailReactor + + var disposeBag = DisposeBag() + + private let bookmarkModalFactory: BookmarkModalFactory + private let collectionSettingFactory: CollectionSettingFactory + private let addCollectionFactory: AddCollectionFactory + private let collectionEditFactory: CollectionEditFactory + private let dictionaryDetailFactory: DictionaryDetailFactory + + private var mainView: CollectionDetailView + + init( + reactor: CollectionDetailReactor, + bookmarkModalFactory: BookmarkModalFactory, + collectionSettingFactory: CollectionSettingFactory, + addCollectionFactory: AddCollectionFactory, + collectionEditFactory: CollectionEditFactory, + dictionaryDetailFactory: DictionaryDetailFactory + ) { + self.mainView = CollectionDetailView(navTitle: reactor.currentState.collection.name) + self.bookmarkModalFactory = bookmarkModalFactory + self.collectionSettingFactory = collectionSettingFactory + self.addCollectionFactory = addCollectionFactory + self.collectionEditFactory = collectionEditFactory + self.dictionaryDetailFactory = dictionaryDetailFactory + super.init() + self.reactor = reactor + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(mainView) + mainView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.horizontalEdges.bottom.equalToSuperview() + } + configureUI() + } +} + +private extension CollectionDetailViewController { + func configureUI() { + mainView.listCollectionView.collectionViewLayout = createListLayout() + mainView.listCollectionView.delegate = self + mainView.listCollectionView.dataSource = self + mainView.listCollectionView.register(DictionaryListCell.self, forCellWithReuseIdentifier: DictionaryListCell.identifier) + } + + func createListLayout() -> UICollectionViewLayout { + let layout = CompositionalLayoutBuilder() + .section { _ in LayoutFactory.getDictionaryListLayout() } + .build() + layout.register(Neutral300DividerView.self, forDecorationViewOfKind: Neutral300DividerView.identifier) + return layout + } +} + +extension CollectionDetailViewController { + func bind(reactor: Reactor) { + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.emptyView.bookmarkButton.rx.tap + .map { Reactor.Action.bookmarkButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.navigation.leftButton.rx.tap + .map { Reactor.Action.backButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.navigation.editButton.rx.tap + .map { Reactor.Action.editButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.navigation.addButton.rx.tap + .map { Reactor.Action.addButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .map(\.collection.recentBookmarks) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, items in + owner.mainView.listCollectionView.reloadData() + owner.mainView.isEmptyData(isEmpty: items.isEmpty) + }) + .disposed(by: disposeBag) + + reactor.state + .map(\.collection.name) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, name in + owner.mainView.setName(name: name) + }) + .disposed(by: disposeBag) + + reactor.pulse(\.$collectionMenu) + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, menu in + switch menu { + case .editBookmark: + let vc = owner.collectionEditFactory.make(bookmarks: reactor.currentState.collection.recentBookmarks) + owner.navigationController?.pushViewController(vc, animated: true) + case .editName: + let vc = owner.addCollectionFactory.make(collection: reactor.currentState.collection) + guard let vc = vc as? AddCollectionViewController else { return } + vc.dismissed + .subscribe { name in + reactor.action.onNext(.changeName(name)) + } + .disposed(by: owner.disposeBag) + owner.present(vc, animated: true) + case .delete: + GuideAlertFactory.show( + mainText: "컬렉션을 삭제하시겠어요?", + ctaText: "삭제하기", + cancelText: "취소", + ctaAction: { reactor.action.onNext(.deleteCollection) }, + cancelAction: {} + ) + default: + break + } + }) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, route in + switch route { + case .dismiss: + owner.navigationController?.popViewController(animated: true) + case .edit: + let vc = owner.collectionSettingFactory.make(setEditMenu: { menu in + owner.reactor?.action.onNext(.selectSetting(menu)) + }) + owner.presentModal(vc) + case .detail(let type, let id): + let vc = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: nil, loginRelay: nil) + owner.navigationController?.pushViewController(vc, animated: true) + default: + break + } + } + .disposed(by: disposeBag) + } +} + +extension CollectionDetailViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + reactor?.currentState.collection.recentBookmarks.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DictionaryListCell.identifier, for: indexPath) as? DictionaryListCell, + let item = reactor?.currentState.collection.recentBookmarks[indexPath.row] + else { + return UICollectionViewCell() + } + var subText: String? { + [.item, .monster, .quest].contains(item.type) ? item.level.map { "Lv. \($0)" } : nil + } + cell.inject( + type: .bookmark, + input: DictionaryListCell.Input( + type: item.type, + mainText: item.name, + subText: subText, + imageUrl: item.imageUrl ?? "", + isBookmarked: true + ), + indexPath: indexPath, + collectionView: collectionView, + onBookmarkTapped: {} + ) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + reactor?.action.onNext(.dataTapped(indexPath.row)) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditFactoryImpl.swift new file mode 100644 index 00000000..910c97b2 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditFactoryImpl.swift @@ -0,0 +1,18 @@ +import MLSBookmarkFeatureInterface +import MLSCore + +public final class CollectionEditFactoryImpl: CollectionEditFactory { + private let bookmarkModalFactory: BookmarkModalFactory + + public init(bookmarkModalFactory: BookmarkModalFactory) { + self.bookmarkModalFactory = bookmarkModalFactory + } + + public func make(bookmarks: [BookmarkResponse]) -> BaseViewController { + let reactor = CollectionEditReactor(bookmarks: bookmarks) + let viewController = CollectionEditViewController(bookmarkModalFactory: bookmarkModalFactory) + viewController.reactor = reactor + viewController.isBottomTabbarHidden = true + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditReactor.swift new file mode 100644 index 00000000..f5c517b8 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditReactor.swift @@ -0,0 +1,63 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class CollectionEditReactor: Reactor { + enum Route { + case none + case dismiss + case collectionList + } + + enum Action { + case backButtonTapped + case addCollectionButtonTapped + case itemTapped(Int) + } + + enum Mutation { + case navigateTo(Route) + case checkBookmarks([BookmarkResponse]) + } + + struct State { + @Pulse var route: Route + var bookmarks: [BookmarkResponse] + var selectedItems = [BookmarkResponse]() + } + + var initialState: State + + init(bookmarks: [BookmarkResponse]) { + self.initialState = State(route: .none, bookmarks: bookmarks) + } + + func mutate(action: Action) -> Observable { + switch action { + case .backButtonTapped: + return .just(.navigateTo(.dismiss)) + case .addCollectionButtonTapped: + return .just(.navigateTo(.collectionList)) + case .itemTapped(let index): + let item = currentState.bookmarks[index] + var newItems = currentState.selectedItems + if let idx = newItems.firstIndex(where: { $0.originalId == item.originalId }) { + newItems.remove(at: idx) + } else { + newItems.append(item) + } + return .just(.checkBookmarks(newItems)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .navigateTo(let route): + newState.route = route + case .checkBookmarks(let bookmarks): + newState.selectedItems = bookmarks + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditView.swift new file mode 100644 index 00000000..13c43a62 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditView.swift @@ -0,0 +1,92 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionEditView: UIView { + private enum Constant { + static let imageViewSize: CGFloat = 44 + static let iconSize: CGFloat = 24 + static let horizontalMargin: CGFloat = 16 + static let topMargin: CGFloat = 12 + static let bottomMargin: CGFloat = 14 + } + + private lazy var headerView: UIView = { + let view = UIView() + view.addSubview(cancelButton) + cancelButton.snp.makeConstraints { make in + make.center.equalToSuperview() + make.size.equalTo(Constant.iconSize) + } + return view + }() + + let cancelButton: UIButton = { + let button = UIButton() + button.setImage(DesignSystemAsset.image(named: "largeX"), for: .normal) + return button + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .korFont(style: .semiBold, size: 16), text: "리스트 편집") + return label + }() + + let listCollectionView: UICollectionView = { + UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + }() + + private let divider = DividerView() + let addButtton = CommonButton(style: .normal, title: "컬렉션에 추가하기", disabledTitle: nil) + + init() { + super.init(frame: .zero) + addViews() + setupConstraints() + backgroundColor = .whiteMLS + listCollectionView.backgroundColor = .neutral100 + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +private extension CollectionEditView { + func addViews() { + addSubview(headerView) + addSubview(titleLabel) + addSubview(listCollectionView) + addSubview(divider) + addSubview(addButtton) + } + + func setupConstraints() { + headerView.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide) + make.leading.equalToSuperview() + make.size.equalTo(Constant.imageViewSize) + } + + titleLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalTo(headerView) + } + + listCollectionView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom) + make.horizontalEdges.equalToSuperview() + } + + divider.snp.makeConstraints { make in + make.top.equalTo(listCollectionView.snp.bottom) + make.horizontalEdges.equalToSuperview() + } + + addButtton.snp.makeConstraints { make in + make.top.equalTo(divider.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + make.bottom.equalToSuperview().inset(Constant.bottomMargin) + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditViewController.swift new file mode 100644 index 00000000..eb836cde --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionEdit/CollectionEditViewController.swift @@ -0,0 +1,144 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxCocoa +import RxSwift +import UIKit + +final class CollectionEditViewController: BaseViewController, View { + typealias Reactor = CollectionEditReactor + + var disposeBag = DisposeBag() + private let bookmarkModalFactory: BookmarkModalFactory + private var mainView = CollectionEditView() + + init(bookmarkModalFactory: BookmarkModalFactory) { + self.bookmarkModalFactory = bookmarkModalFactory + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(mainView) + mainView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + configureUI() + } +} + +private extension CollectionEditViewController { + func configureUI() { + mainView.listCollectionView.collectionViewLayout = createListLayout() + mainView.listCollectionView.delegate = self + mainView.listCollectionView.dataSource = self + mainView.listCollectionView.register(DictionaryListCell.self, forCellWithReuseIdentifier: DictionaryListCell.identifier) + } + + func createListLayout() -> UICollectionViewLayout { + CompositionalLayoutBuilder() + .section { _ in LayoutFactory.getCollectionListEditLayout() } + .build() + } +} + +extension CollectionEditViewController { + func bind(reactor: Reactor) { + mainView.cancelButton.rx.tap + .map { Reactor.Action.backButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.addButtton.rx.tap + .map { Reactor.Action.addCollectionButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .map(\.bookmarks) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, _ in + owner.mainView.listCollectionView.reloadData() + } + .disposed(by: disposeBag) + + reactor.state + .map(\.selectedItems) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, _ in + owner.mainView.listCollectionView.reloadData() + } + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, route in + switch route { + case .dismiss: + owner.navigationController?.popViewController(animated: true) + case .collectionList: + let vc = owner.bookmarkModalFactory.make( + bookmarkIds: reactor.currentState.selectedItems.map { $0.bookmarkId } + ) { isSave in + if isSave { + owner.navigationController?.popToRootViewController(animated: true) + } + } + owner.present(vc, animated: true) + default: + break + } + } + .disposed(by: disposeBag) + } +} + +extension CollectionEditViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + reactor?.currentState.bookmarks.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DictionaryListCell.identifier, for: indexPath) as? DictionaryListCell, + let item = reactor?.currentState.bookmarks[indexPath.row] + else { + return UICollectionViewCell() + } + let isSelected = reactor?.currentState.selectedItems.contains(where: { $0.originalId == item.originalId }) ?? false + var subText: String? { + [.item, .monster, .quest].contains(item.type) ? item.level.map { "Lv. \($0)" } : nil + } + cell.inject( + type: .checkbox, + input: DictionaryListCell.Input( + type: item.type, + mainText: item.name, + subText: subText, + imageUrl: item.imageUrl ?? "", + isBookmarked: isSelected + ), + indexPath: indexPath, + collectionView: collectionView, + onBookmarkTapped: { [weak self] in + self?.reactor?.action.onNext(.itemTapped(indexPath.row)) + } + ) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + reactor?.action.onNext(.itemTapped(indexPath.row)) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListCell.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListCell.swift new file mode 100644 index 00000000..639c520c --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListCell.swift @@ -0,0 +1,50 @@ +import MLSCore +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionListCell: UICollectionViewCell { + let cellView = CollectionList() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(cellView) + cellView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +extension CollectionListCell { + struct Input { + let title: String + let count: Int + let images: [String?] + } + + func inject(input: Input) { + loadImages(from: input.images) { [weak self] images in + self?.cellView.setImages(images: images) + } + cellView.setTitle(text: input.title) + cellView.setSubtitle(text: "\(input.count)개") + } +} + +private func loadImages(from urls: [String?], completion: @escaping ([UIImage?]) -> Void) { + var results = [UIImage?](repeating: nil, count: urls.count) + let group = DispatchGroup() + for (index, urlString) in urls.enumerated() { + group.enter() + ImageLoader.shared.loadImage(stringURL: urlString) { image in + results[index] = image + group.leave() + } + } + group.notify(queue: .main) { + completion(results) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListEmptyView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListEmptyView.swift new file mode 100644 index 00000000..1bfc5706 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListEmptyView.swift @@ -0,0 +1,56 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionListEmptyView: UIView { + private enum Constant { + static let topInset: CGFloat = 60 + static let imageSize: CGFloat = 220 + static let textSpacing: CGFloat = 10 + } + + let imageView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "fabHint") + return view + }() + + private let mainLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .h_xl_b, text: "첫 컬렉션을 만들어보세요", color: .textColor) + return label + }() + + private let subLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .cp_s_r, text: "컬렉션을 만들면 북마크한 리스트를\n내 취향대로 정리할 수 있어요.", color: .neutral600) + label.numberOfLines = 2 + return label + }() + + init() { + super.init(frame: .zero) + addSubview(imageView) + addSubview(mainLabel) + addSubview(subLabel) + + imageView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(Constant.topInset) + make.centerX.equalToSuperview() + make.size.equalTo(Constant.imageSize) + } + + mainLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom) + make.centerX.equalToSuperview() + } + + subLabel.snp.makeConstraints { make in + make.top.equalTo(mainLabel.snp.bottom).offset(Constant.textSpacing) + make.centerX.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListFactoryImpl.swift new file mode 100644 index 00000000..ed1a23f8 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListFactoryImpl.swift @@ -0,0 +1,32 @@ +import MLSBookmarkFeatureInterface +import MLSCore + +public final class CollectionListFactoryImpl: CollectionListFactory { + private let collectionRepository: CollectionRepository + private let addCollectionFactory: AddCollectionFactory + private let collectionDetailFactory: CollectionDetailFactory + private let sortedBottomSheetFactory: SortedBottomSheetFactory + + public init( + collectionRepository: CollectionRepository, + addCollectionFactory: AddCollectionFactory, + collectionDetailFactory: CollectionDetailFactory, + sortedBottomSheetFactory: SortedBottomSheetFactory + ) { + self.collectionRepository = collectionRepository + self.addCollectionFactory = addCollectionFactory + self.collectionDetailFactory = collectionDetailFactory + self.sortedBottomSheetFactory = sortedBottomSheetFactory + } + + public func make() -> BaseViewController { + let reactor = CollectionListReactor(collectionRepository: collectionRepository) + let viewController = CollectionListViewController( + addCollectionFactory: addCollectionFactory, + detailFactory: collectionDetailFactory, + sortedBottomSheetFactory: sortedBottomSheetFactory + ) + viewController.reactor = reactor + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListReactor.swift new file mode 100644 index 00000000..a635b666 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListReactor.swift @@ -0,0 +1,73 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class CollectionListReactor: Reactor { + enum Route { + case none + case detail(CollectionResponse) + case sortFilter + } + + enum Action { + case itemTapped(Int) + case viewWillAppear + case completeAdding + case sortButtonTapped + case sortOptionSelected(SortType) + } + + enum Mutation { + case navigateTo(Route) + case setListData([CollectionResponse]) + case setSort(SortType) + } + + struct State { + @Pulse var route: Route + var collectionList: [CollectionResponse] + var sortFilter: [SortType] = [.korean, .latest] + var selectedSort: SortType? + } + + var initialState: State + private let collectionRepository: CollectionRepository + + init(collectionRepository: CollectionRepository) { + self.collectionRepository = collectionRepository + self.initialState = State(route: .none, collectionList: []) + } + + func mutate(action: Action) -> Observable { + switch action { + case .viewWillAppear: + return collectionRepository.fetchCollectionList(sort: currentState.selectedSort?.sortParameter) + .map { .setListData($0) } + case .itemTapped(let index): + return .just(.navigateTo(.detail(currentState.collectionList[index]))) + case .completeAdding: + return collectionRepository.fetchCollectionList(sort: currentState.selectedSort?.sortParameter) + .map { .setListData($0) } + case .sortButtonTapped: + return .just(.navigateTo(.sortFilter)) + case .sortOptionSelected(let sort): + return Observable.concat([ + .just(.setSort(sort)), + collectionRepository.fetchCollectionList(sort: sort.sortParameter).map { .setListData($0) } + ]) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setListData(let data): + newState.collectionList = data + case .navigateTo(let route): + newState.route = route + case .setSort(let sort): + newState.selectedSort = sort + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListView.swift new file mode 100644 index 00000000..16bbab66 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListView.swift @@ -0,0 +1,115 @@ +import MLSBookmarkFeatureInterface +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionListView: UIView { + private enum Constant { + static let stackViewSpacing: CGFloat = 12 + static let topMargin: CGFloat = 12 + static let filterHeight: CGFloat = 32 + static let horizontalMargin: CGFloat = 16 + static let nonFilterTopMargin: CGFloat = 20 + } + + let listCollectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + return collectionView + }() + + private lazy var filterStackView: UIStackView = { + let view = UIStackView(arrangedSubviews: [sortButton]) + view.axis = .horizontal + view.spacing = Constant.stackViewSpacing + view.distribution = .fillProportionally + return view + }() + + lazy var sortButton: UIButton = { + let button = UIButton() + button.setAttributedTitle(.makeStyledString(font: .b_s_r, text: "최신 순", color: .textColor), for: .normal) + button.setImage(DesignSystemAsset.image(named: "lineArrowDown").withRenderingMode(.alwaysTemplate), for: .normal) + button.tintColor = .textColor + button.semanticContentAttribute = .forceRightToLeft + return button + }() + + let emptyView: CollectionListEmptyView = { + let view = CollectionListEmptyView() + view.isHidden = true + return view + }() + + init() { + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +private extension CollectionListView { + func addViews() { + addSubview(filterStackView) + addSubview(listCollectionView) + addSubview(emptyView) + } + + func setupConstraints() { + filterStackView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(Constant.topMargin) + make.trailing.equalToSuperview().inset(Constant.horizontalMargin) + } + + sortButton.snp.makeConstraints { make in + make.height.equalTo(Constant.filterHeight) + } + + listCollectionView.snp.makeConstraints { make in + make.top.equalTo(filterStackView.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.bottom.equalToSuperview() + } + + emptyView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configureUI() { + backgroundColor = .neutral100 + listCollectionView.backgroundColor = .neutral100 + } +} + +extension CollectionListView { + func updateView(isEmptyData: Bool) { + emptyView.isHidden = !isEmptyData + filterStackView.isHidden = isEmptyData + listCollectionView.isHidden = isEmptyData + + if isEmptyData { + listCollectionView.snp.remakeConstraints { make in + make.top.equalToSuperview().inset(Constant.nonFilterTopMargin) + make.horizontalEdges.bottom.equalToSuperview() + } + } else { + filterStackView.snp.remakeConstraints { make in + make.top.equalToSuperview().inset(Constant.topMargin) + make.trailing.equalToSuperview().inset(Constant.horizontalMargin) + } + + listCollectionView.snp.remakeConstraints { make in + make.top.equalTo(filterStackView.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.bottom.equalToSuperview() + } + } + } + + func selectSort(selectedType: SortType) { + sortButton.setAttributedTitle(.makeStyledString(font: .b_s_r, text: selectedType.rawValue, color: .primary700), for: .normal) + sortButton.tintColor = .primary700 + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListViewController.swift new file mode 100644 index 00000000..94896690 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionList/CollectionListViewController.swift @@ -0,0 +1,159 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxSwift +import UIKit + +final class CollectionListViewController: BaseViewController, View { + typealias Reactor = CollectionListReactor + + var disposeBag = DisposeBag() + private var selectedSortIndex = 0 + + private let addCollectionFactory: AddCollectionFactory + private let detailFactory: CollectionDetailFactory + private let sortedBottomSheetFactory: SortedBottomSheetFactory + + private var mainView = CollectionListView() + + init( + addCollectionFactory: AddCollectionFactory, + detailFactory: CollectionDetailFactory, + sortedBottomSheetFactory: SortedBottomSheetFactory + ) { + self.addCollectionFactory = addCollectionFactory + self.detailFactory = detailFactory + self.sortedBottomSheetFactory = sortedBottomSheetFactory + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func viewDidLoad() { + super.viewDidLoad() + addViews() + setupConstraints() + configureUI() + } +} + +private extension CollectionListViewController { + func addViews() { + view.addSubview(mainView) + } + + func setupConstraints() { + mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configureUI() { + mainView.listCollectionView.collectionViewLayout = createListLayout() + mainView.listCollectionView.delegate = self + mainView.listCollectionView.dataSource = self + mainView.listCollectionView.register(CollectionListCell.self, forCellWithReuseIdentifier: CollectionListCell.identifier) + + addFloatingButton { [weak self] in + guard let self else { return } + let viewController = self.addCollectionFactory.make(collection: nil) + guard let vc = viewController as? AddCollectionViewController else { return } + vc.dismissed + .withUnretained(self) + .subscribe { owner, _ in + owner.reactor?.action.onNext(.completeAdding) + } + .disposed(by: self.disposeBag) + self.present(vc, animated: true) + } + } + + func createListLayout() -> UICollectionViewLayout { + CompositionalLayoutBuilder() + .section { _ in LayoutFactory.getCollectionListLayout() } + .build() + } +} + +extension CollectionListViewController { + func bind(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + mainView.sortButton.rx.tap + .map { .sortButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, route in + switch route { + case .detail(let collection): + let viewController = owner.detailFactory.make(collection: collection, onMoveToMain: { + if let tabBarController = owner.tabBarController as? BottomTabBarController { + tabBarController.selectTab(index: 0) + DictionaryTabRegistry.changeTab(index: 0) + } + }) + owner.tabBarController?.navigationController?.pushViewController(viewController, animated: true) + case .sortFilter: + let viewController = owner.sortedBottomSheetFactory.make( + sortedOptions: reactor.currentState.sortFilter, + selectedIndex: owner.selectedSortIndex + ) { index in + owner.selectedSortIndex = index + let selectedFilter = reactor.currentState.sortFilter[index] + reactor.action.onNext(.sortOptionSelected(selectedFilter)) + owner.mainView.selectSort(selectedType: selectedFilter) + } + owner.tabBarController?.presentModal(viewController) + default: + break + } + } + .disposed(by: disposeBag) + + reactor.state + .map(\.collectionList) + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, collectionList in + owner.mainView.updateView(isEmptyData: collectionList.isEmpty) + owner.mainView.listCollectionView.reloadData() + } + .disposed(by: disposeBag) + } +} + +extension CollectionListViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + reactor?.currentState.collectionList.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionListCell.identifier, for: indexPath) as? CollectionListCell, + let item = reactor?.currentState.collectionList[indexPath.row] + else { + return UICollectionViewCell() + } + cell.inject(input: CollectionListCell.Input( + title: item.name, + count: item.recentBookmarks.count, + images: item.recentBookmarks.map { $0.imageUrl } + )) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + reactor?.action.onNext(.itemTapped(indexPath.row)) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingFactoryImpl.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingFactoryImpl.swift new file mode 100644 index 00000000..f35ac896 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingFactoryImpl.swift @@ -0,0 +1,14 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem + +public final class CollectionSettingFactoryImpl: CollectionSettingFactory { + public init() {} + + public func make(setEditMenu: ((CollectionSettingMenu) -> Void)?) -> BaseViewController & ModalPresentable { + let viewController = CollectionSettingViewController() + viewController.reactor = CollectionSettingReactor() + viewController.setMenu = setEditMenu + return viewController + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingReactor.swift new file mode 100644 index 00000000..ebe9a5a9 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingReactor.swift @@ -0,0 +1,55 @@ +import MLSBookmarkFeatureInterface +import ReactorKit +import RxSwift + +final class CollectionSettingReactor: Reactor { + enum Route { + case none + case dismiss + case dismissWithMenu(CollectionSettingMenu) + } + + enum Action { + case cancelButtonTapped + case editBookmarkButtonTapped + case editNameButtonTapped + case deleteButtonTapped + } + + enum Mutation { + case navigateTo(Route) + } + + struct State { + @Pulse var route: Route = .none + var menu = CollectionSettingMenu.allCases + } + + var initialState: State + + init() { + self.initialState = State() + } + + func mutate(action: Action) -> Observable { + switch action { + case .cancelButtonTapped: + return .just(.navigateTo(.dismiss)) + case .editBookmarkButtonTapped: + return .just(.navigateTo(.dismissWithMenu(.editBookmark))) + case .editNameButtonTapped: + return .just(.navigateTo(.dismissWithMenu(.editName))) + case .deleteButtonTapped: + return .just(.navigateTo(.dismissWithMenu(.delete))) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .navigateTo(let route): + newState.route = route + } + return newState + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingView.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingView.swift new file mode 100644 index 00000000..061934c4 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingView.swift @@ -0,0 +1,40 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class CollectionSettingView: UIView { + private enum Constant { + static let topInset: CGFloat = 16 + static let tableViewInset: CGFloat = 14 + } + + let header: Header = { + Header(style: .filter, title: "컬렉션") + }() + + let menuTableView: UITableView = { + let view = UITableView() + view.isScrollEnabled = false + view.separatorStyle = .none + return view + }() + + init() { + super.init(frame: .zero) + addSubview(header) + addSubview(menuTableView) + + header.snp.makeConstraints { make in + make.top.equalToSuperview().inset(Constant.topInset) + make.horizontalEdges.equalToSuperview() + } + + menuTableView.snp.makeConstraints { make in + make.top.equalTo(header.snp.bottom).offset(Constant.tableViewInset) + make.horizontalEdges.bottom.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingViewController.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingViewController.swift new file mode 100644 index 00000000..bdf3b951 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/CollectionSettingSheet/CollectionSettingViewController.swift @@ -0,0 +1,92 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit +import UIKit + +final class CollectionSettingViewController: BaseViewController, ModalPresentable, View { + var modalHeight: CGFloat? = 284 + + typealias Reactor = CollectionSettingReactor + + var disposeBag = DisposeBag() + var setMenu: ((CollectionSettingMenu) -> Void)? + + private var mainView = CollectionSettingView() + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(mainView) + mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + mainView.menuTableView.delegate = self + mainView.menuTableView.dataSource = self + } + + func bind(reactor: Reactor) { + mainView.header.firstIconButton.rx.tap + .map { Reactor.Action.cancelButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .subscribe { owner, route in + switch route { + case .dismiss: + owner.dismissCurrentModal() + case .dismissWithMenu(let menu): + owner.setMenu?(menu) + owner.dismissCurrentModal() + default: + break + } + } + .disposed(by: disposeBag) + } +} + +extension CollectionSettingViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + reactor?.currentState.menu.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = UITableViewCell() + guard let item = reactor?.currentState.menu[indexPath.row] else { return cell } + cell.textLabel?.textAlignment = .center + cell.textLabel?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + cell.textLabel?.attributedText = .makeStyledString(font: .b_m_r, text: item.title, color: item.titleColor) + if indexPath.row < (reactor?.currentState.menu.count ?? 0) - 1 { + let divider = UIView() + divider.backgroundColor = .lightGray.withAlphaComponent(0.3) + cell.contentView.addSubview(divider) + divider.snp.makeConstraints { make in + make.horizontalEdges.bottom.equalToSuperview() + make.height.equalTo(1) + } + } + cell.selectionStyle = .none + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let item = reactor?.currentState.menu[indexPath.row] else { return } + switch item { + case .editBookmark: reactor?.action.onNext(.editBookmarkButtonTapped) + case .editName: reactor?.action.onNext(.editNameButtonTapped) + case .delete: reactor?.action.onNext(.deleteButtonTapped) + case .cancel: reactor?.action.onNext(.cancelButtonTapped) + } + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 54 + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/AddFolderCell.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/AddFolderCell.swift new file mode 100644 index 00000000..f9b35472 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/AddFolderCell.swift @@ -0,0 +1,67 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class AddFolderCell: UICollectionViewCell { + private enum Constant { + static let iconInset: CGFloat = 8 + static let radius: CGFloat = 8 + static let margin: CGFloat = 16 + static let buttonSize: CGFloat = 40 + } + + private lazy var addIconView: UIView = { + let view = UIView() + view.layer.cornerRadius = Constant.radius + view.backgroundColor = .primary100 + + view.addSubview(iconView) + iconView.snp.makeConstraints { make in + make.center.equalTo(view).inset(Constant.iconInset) + } + return view + }() + + private let iconView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "addIcon").withRenderingMode(.alwaysTemplate) + view.tintColor = .whiteMLS + return view + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .b_m_r, text: "새로운 컬렉션 추가하기", alignment: .left) + return label + }() + + private let divider: UIView = { + let view = UIView() + view.backgroundColor = .neutral100 + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(addIconView) + contentView.addSubview(titleLabel) + contentView.addSubview(divider) + + addIconView.snp.makeConstraints { make in + make.leading.verticalEdges.equalToSuperview().inset(Constant.margin) + make.size.equalTo(Constant.buttonSize) + } + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(addIconView.snp.trailing).offset(Constant.margin) + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(Constant.margin) + } + divider.snp.makeConstraints { make in + make.height.equalTo(1) + make.horizontalEdges.bottom.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/DictionaryListCell.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/DictionaryListCell.swift new file mode 100644 index 00000000..ffac662e --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/DictionaryListCell.swift @@ -0,0 +1,90 @@ +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import SnapKit +import UIKit + +final class DictionaryListCell: UICollectionViewCell { + private var onBookmarkTapped: (() -> Void)? + + let cellView = CardList() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(cellView) + cellView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func prepareForReuse() { + super.prepareForReuse() + onBookmarkTapped = nil + cellView.onIconTapped = nil + cellView.setMainText(text: "") + cellView.setSubText(text: nil) + cellView.setSelected(isSelected: false) + } +} + +extension DictionaryListCell { + struct Input { + let type: DictionaryItemType + let mainText: String + let subText: String? + let imageUrl: String + let isBookmarked: Bool + } + + func inject( + type: CardList.CardListType, + input: Input, + indexPath: IndexPath, + collectionView: UICollectionView, + isMap: Bool = false, + onBookmarkTapped: @escaping () -> Void + ) { + cellView.setType(type: type) + cellView.setImage(image: UIImage(), backgroundColor: input.type.backgroundColor) + + if let url = URL(string: input.imageUrl) { + ImageLoader.shared.loadImage(url: url) { [weak self] image in + guard let self else { return } + if let currentIndex = collectionView.indexPath(for: self), currentIndex == indexPath { + if isMap { + self.cellView.setMapImage(image: image ?? UIImage(), backgroundColor: input.type.backgroundColor) + } else { + self.cellView.setImage(image: image ?? UIImage(), backgroundColor: input.type.backgroundColor) + } + } + } + } + + cellView.setMainText(text: input.mainText) + cellView.setSubText(text: input.subText) + cellView.setSelected(isSelected: input.isBookmarked) + self.onBookmarkTapped = onBookmarkTapped + cellView.onIconTapped = { [weak self] in + self?.onBookmarkTapped?() + } + } + + func updateBookmarkState(isBookmarked: Bool) { + cellView.setSelected(isSelected: isBookmarked) + } +} + +extension DictionaryItemType { + var backgroundColor: UIColor { + switch self { + case .item: .listItem + case .monster: .listMonster + case .map: .listMap + case .npc: .listNPC + case .quest: .listQuest + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/FolderCell.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/FolderCell.swift new file mode 100644 index 00000000..e159eca3 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/FolderCell.swift @@ -0,0 +1,93 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class FolderCell: UICollectionViewCell { + private enum Constant { + static let iconInset: CGFloat = 8 + static let radius: CGFloat = 8 + static let margin: CGFloat = 16 + static let iconSize: CGFloat = 24 + static let buttonSize: CGFloat = 40 + } + + private lazy var imageContainerView: UIView = { + let view = UIView() + view.layer.cornerRadius = Constant.radius + view.backgroundColor = .neutral200 + + view.addSubview(iconView) + iconView.snp.makeConstraints { make in + make.center.equalTo(view).inset(Constant.iconInset) + } + return view + }() + + private let iconView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "bookmark").withRenderingMode(.alwaysTemplate) + view.tintColor = .neutral300 + return view + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.lineBreakMode = .byTruncatingTail + return label + }() + + let checkBoxButton: UIButton = { + let button = UIButton() + button.setImage(DesignSystemAsset.image(named: "checkSquareFill").withRenderingMode(.alwaysTemplate), for: .normal) + button.tintColor = .neutral300 + button.isUserInteractionEnabled = false + return button + }() + + private let divider: UIView = { + let view = UIView() + view.backgroundColor = .neutral100 + return view + }() + + var isChecked: Bool = false { + didSet { + checkBoxButton.tintColor = isChecked ? .primary700 : .neutral300 + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(imageContainerView) + contentView.addSubview(titleLabel) + contentView.addSubview(checkBoxButton) + contentView.addSubview(divider) + + imageContainerView.snp.makeConstraints { make in + make.leading.verticalEdges.equalToSuperview().inset(Constant.margin) + make.size.equalTo(Constant.buttonSize) + } + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(imageContainerView.snp.trailing).offset(Constant.margin) + make.centerY.equalToSuperview() + } + checkBoxButton.snp.makeConstraints { make in + make.leading.equalTo(titleLabel.snp.trailing).offset(Constant.margin) + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(Constant.margin) + make.size.equalTo(Constant.iconSize) + } + divider.snp.makeConstraints { make in + make.height.equalTo(1) + make.horizontalEdges.bottom.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + func inject(title: String?) { + titleLabel.attributedText = .makeStyledString(font: .b_m_r, text: title, alignment: .left) + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/PageTabbarCell.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/PageTabbarCell.swift new file mode 100644 index 00000000..7a8e1d04 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/Common/PageTabbarCell.swift @@ -0,0 +1,38 @@ +import MLSDesignSystem +import SnapKit +import UIKit + +final class PageTabbarCell: UICollectionViewCell { + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .b_m_r + label.textColor = .neutral600 + label.numberOfLines = 1 + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.8 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview() + make.centerY.equalToSuperview() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override var isSelected: Bool { + didSet { + titleLabel.font = isSelected ? .sub_m_b : .b_m_r + titleLabel.textColor = isSelected ? .textColor : .neutral600 + } + } + + func inject(title: String?) { + titleLabel.text = title + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/BookmarkResponse.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/BookmarkResponse.swift new file mode 100644 index 00000000..e9934cd5 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/BookmarkResponse.swift @@ -0,0 +1,17 @@ +public struct BookmarkResponse: Equatable { + public let name: String + public let bookmarkId: Int + public let originalId: Int + public let imageUrl: String? + public let type: DictionaryItemType + public let level: Int? + + public init(name: String, bookmarkId: Int, originalId: Int, imageUrl: String?, type: DictionaryItemType, level: Int?) { + self.name = name + self.bookmarkId = bookmarkId + self.originalId = originalId + self.imageUrl = imageUrl + self.type = type + self.level = level + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionResponse.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionResponse.swift new file mode 100644 index 00000000..a69ddd5c --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionResponse.swift @@ -0,0 +1,13 @@ +public struct CollectionResponse: Equatable { + public let collectionId: Int + public var name: String + public let createdAt: [Int] + public var recentBookmarks: [BookmarkResponse] + + public init(collectionId: Int, name: String, createdAt: [Int], recentBookmarks: [BookmarkResponse]) { + self.collectionId = collectionId + self.name = name + self.createdAt = createdAt + self.recentBookmarks = recentBookmarks + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionSettingMenu.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionSettingMenu.swift new file mode 100644 index 00000000..c02404cc --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/CollectionSettingMenu.swift @@ -0,0 +1,24 @@ +import UIKit + +public enum CollectionSettingMenu: CaseIterable { + case editBookmark + case editName + case delete + case cancel + + public var title: String { + switch self { + case .editBookmark: return "컬렉션 북마크 수정" + case .editName: return "컬렉션 이름 수정" + case .delete: return "컬렉션 삭제" + case .cancel: return "취소" + } + } + + public var titleColor: UIColor { + switch self { + case .delete: return .red + default: return .black + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryItemType.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryItemType.swift new file mode 100644 index 00000000..f43d1d11 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryItemType.swift @@ -0,0 +1,17 @@ +public enum DictionaryItemType: String { + case item + case monster + case map + case npc + case quest + + public var toDictionaryType: DictionaryType? { + switch self { + case .item: return .item + case .monster: return .monster + case .map: return .map + case .npc: return .npc + case .quest: return .quest + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryMainViewType.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryMainViewType.swift new file mode 100644 index 00000000..9ad16018 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryMainViewType.swift @@ -0,0 +1,16 @@ +public enum DictionaryMainViewType { + case main + case search + case bookmark + + public var pageTabList: [DictionaryType] { + switch self { + case .main: + return [.total, .monster, .item, .map, .npc, .quest] + case .search: + return [.total, .monster, .item, .map, .npc, .quest] + case .bookmark: + return [.total, .collection, .monster, .item, .map, .npc, .quest] + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryType.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryType.swift new file mode 100644 index 00000000..2789524f --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/DictionaryType.swift @@ -0,0 +1,49 @@ +public enum DictionaryType: String, CaseIterable { + case total + case collection + case item + case monster + case map + case npc + case quest + + public var title: String { + switch self { + case .total: return "전체" + case .collection: return "컬렉션" + case .item: return "아이템" + case .monster: return "몬스터" + case .map: return "맵" + case .npc: return "NPC" + case .quest: return "퀘스트" + } + } + + public var bookmarkSortedFilter: [SortType] { + switch self { + case .total: return [.korean, .latest] + case .item: return [.korean, .levelDESC, .levelASC] + case .monster: return [.korean, .levelDESC, .levelASC] + case .map: return [.korean, .mostAppear] + case .npc: return [.korean] + case .quest: return [.korean, .levelLowest, .levelHighest] + case .collection: return [] + } + } + + public var isBookmarkSortHidden: Bool { + return bookmarkSortedFilter.count == 0 + } + + public var tabIndex: Int { + switch self { + case .total: return 0 + case .collection: return 1 + case .monster: return 2 + case .item: return 3 + case .map: return 4 + case .npc: return 5 + case .quest: return 6 + } + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/ItemFilterCriteria.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/ItemFilterCriteria.swift new file mode 100644 index 00000000..8fe61cd6 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/ItemFilterCriteria.swift @@ -0,0 +1,13 @@ +public struct ItemFilterCriteria { + public let jobIds: [Int] + public let startLevel: Int? + public let endLevel: Int? + public let categoryIds: [Int] + + public init(jobIds: [Int], startLevel: Int?, endLevel: Int?, categoryIds: [Int]) { + self.jobIds = jobIds + self.startLevel = startLevel + self.endLevel = endLevel + self.categoryIds = categoryIds + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/SortType.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/SortType.swift new file mode 100644 index 00000000..b5848888 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Entities/SortType.swift @@ -0,0 +1,37 @@ +public enum SortType: String { + case korean = "가나다 순" + case levelDESC = "레벨 높은 순" + case levelASC = "레벨 낮은 순" + case expDESC = "획득 경험치 높은 순" + case expASC = "획득 경험치 낮은 순" + case latest = "최신순" + case mostAppear = "출현 많은 순" + case levelLowest = "수락 레벨 낮은 순" + case levelHighest = "수락 레벨 높은 순" + case mostDrop = "드롭률 높은 순" + + public var sortKey: String { + switch self { + case .korean: return "name" + case .levelDESC, .levelASC: return "level" + case .expDESC, .expASC: return "exp" + case .latest: return "createdAt" + case .mostAppear: return "count" + case .levelLowest, .levelHighest: return "minLevel" + case .mostDrop: return "dropRate" + } + } + + public var direction: String { + switch self { + case .korean, .levelASC, .expASC, .latest, .mostAppear, .levelLowest, .mostDrop: + return "asc" + case .levelDESC, .expDESC, .levelHighest: + return "desc" + } + } + + public var sortParameter: String { + return "\(sortKey),\(direction)" + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/AddCollectionFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/AddCollectionFactory.swift new file mode 100644 index 00000000..029875dd --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/AddCollectionFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol AddCollectionFactory { + func make(collection: CollectionResponse?) -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkListFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkListFactory.swift new file mode 100644 index 00000000..4ffb7554 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkListFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol BookmarkListFactory { + func make(type: DictionaryType, listType: DictionaryMainViewType) -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkMainFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkMainFactory.swift new file mode 100644 index 00000000..f1e7b23a --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkMainFactory.swift @@ -0,0 +1,7 @@ +import MLSCore + +import UIKit + +public protocol BookmarkMainFactory { + func make(bottomInset: CGFloat) -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkModalFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkModalFactory.swift new file mode 100644 index 00000000..b26528a5 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkModalFactory.swift @@ -0,0 +1,6 @@ +import MLSCore + +public protocol BookmarkModalFactory { + func make(bookmarkIds: [Int]) -> BaseViewController + func make(bookmarkIds: [Int], onComplete: ((Bool) -> Void)?) -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkOnBoardingFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkOnBoardingFactory.swift new file mode 100644 index 00000000..71aa08ff --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/BookmarkOnBoardingFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol BookmarkOnBoardingFactory { + func make() -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionDetailFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionDetailFactory.swift new file mode 100644 index 00000000..3d6a3f5c --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionDetailFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol CollectionDetailFactory { + func make(collection: CollectionResponse, onMoveToMain: (() -> Void)?) -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionEditFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionEditFactory.swift new file mode 100644 index 00000000..e2986af9 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionEditFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol CollectionEditFactory { + func make(bookmarks: [BookmarkResponse]) -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionListFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionListFactory.swift new file mode 100644 index 00000000..6217a045 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionListFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol CollectionListFactory { + func make() -> BaseViewController +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionSettingFactory.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionSettingFactory.swift new file mode 100644 index 00000000..549fcafb --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/CollectionSettingFactory.swift @@ -0,0 +1,6 @@ +import MLSCore +import MLSDesignSystem + +public protocol CollectionSettingFactory { + func make(setEditMenu: ((CollectionSettingMenu) -> Void)?) -> BaseViewController & ModalPresentable +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/DictionaryExternalFactories.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/DictionaryExternalFactories.swift new file mode 100644 index 00000000..a1ce47d7 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Factories/DictionaryExternalFactories.swift @@ -0,0 +1,43 @@ +import MLSCore +import MLSDesignSystem +import RxRelay + +// DictionaryFeature 모듈이 SPM으로 전환되기 전까지 임시로 정의하는 프로토콜들입니다. +// MLSDictionaryFeature 모듈이 생성되면 해당 모듈의 타입을 사용하도록 교체해야 합니다. + +public protocol DictionarySearchFactory { + func make() -> BaseViewController +} + +public protocol DictionaryNotificationFactory { + func make() -> BaseViewController +} + +public protocol DictionaryDetailFactory { + func make( + type: DictionaryType, + id: Int, + bookmarkRelay: PublishRelay<(id: Int, newBookmarkId: Int?)>?, + loginRelay: PublishRelay? + ) -> BaseViewController +} + +public protocol SortedBottomSheetFactory { + func make( + sortedOptions: [SortType], + selectedIndex: Int, + onSelectedIndex: @escaping (Int) -> Void + ) -> BaseViewController & ModalPresentable +} + +public protocol ItemFilterBottomSheetFactory { + func make(onFilterSelected: @escaping ([(String, String)]) -> Void) -> BaseViewController +} + +public protocol MonsterFilterBottomSheetFactory { + func make( + startLevel: Int, + endLevel: Int, + onFilterSelected: @escaping (Int, Int) -> Void + ) -> BaseViewController & ModalPresentable +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkAuthRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkAuthRepository.swift new file mode 100644 index 00000000..35a32e84 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkAuthRepository.swift @@ -0,0 +1,5 @@ +import RxSwift + +public protocol BookmarkAuthRepository { + func isLoggedIn() -> Observable +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkRepository.swift new file mode 100644 index 00000000..f3f31ff0 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkRepository.swift @@ -0,0 +1,12 @@ +import RxSwift + +public protocol BookmarkRepository { + func setBookmark(resourceId: Int, type: DictionaryItemType) -> Observable + func deleteBookmark(bookmarkId: Int) -> Observable + func fetchBookmark(sort: String?) -> Observable<[BookmarkResponse]> + func fetchMonsterBookmark(minLevel: Int?, maxLevel: Int?, sort: String?) -> Observable<[BookmarkResponse]> + func fetchNPCBookmark(sort: String?) -> Observable<[BookmarkResponse]> + func fetchQuestBookmark(sort: String?) -> Observable<[BookmarkResponse]> + func fetchItemBookmark(jobId: Int?, minLevel: Int?, maxLevel: Int?, categoryIds: [Int]?, sort: String?) -> Observable<[BookmarkResponse]> + func fetchMapBookmark(sort: String?) -> Observable<[BookmarkResponse]> +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkUserDefaultsRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkUserDefaultsRepository.swift new file mode 100644 index 00000000..099d0d7e --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/BookmarkUserDefaultsRepository.swift @@ -0,0 +1,6 @@ +import RxSwift + +public protocol BookmarkUserDefaultsRepository { + func hasVisitedOnboarding() -> Observable + func markOnboardingVisited() -> Completable +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/CollectionRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/CollectionRepository.swift new file mode 100644 index 00000000..d839bb4b --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/Repositories/CollectionRepository.swift @@ -0,0 +1,10 @@ +import RxSwift + +public protocol CollectionRepository { + func fetchCollectionList(sort: String?) -> Observable<[CollectionResponse]> + func createCollectionList(name: String) -> Completable + func fetchCollection(id: Int) -> Observable<[BookmarkResponse]> + func updateCollectionName(collectionId: Int, name: String) -> Completable + func deleteCollection(collectionId: Int) -> Completable + func addCollectionAndBookmark(collectionIds: [Int], bookmarkIds: [Int]) -> Completable +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/UseCases/ParseItemFilterResultUseCase.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/UseCases/ParseItemFilterResultUseCase.swift new file mode 100644 index 00000000..13a68723 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureInterface/UseCases/ParseItemFilterResultUseCase.swift @@ -0,0 +1,3 @@ +public protocol ParseItemFilterResultUseCase { + func execute(results: [(String, String)]) -> ItemFilterCriteria +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkAuthRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkAuthRepository.swift new file mode 100644 index 00000000..f7bef002 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkAuthRepository.swift @@ -0,0 +1,12 @@ +import MLSBookmarkFeatureInterface +import RxSwift + +public final class MockBookmarkAuthRepository: BookmarkAuthRepository { + public var isLoggedInResult: Observable = .just(true) + + public init() {} + + public func isLoggedIn() -> Observable { + isLoggedInResult + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkRepository.swift new file mode 100644 index 00000000..1cc37895 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkRepository.swift @@ -0,0 +1,47 @@ +import MLSBookmarkFeatureInterface +import RxSwift + +public final class MockBookmarkRepository: BookmarkRepository { + public var setBookmarkResult: Observable = .just(0) + public var deleteBookmarkResult: Observable = .just(nil) + public var fetchBookmarkResult: Observable<[BookmarkResponse]> = .just([]) + public var fetchMonsterBookmarkResult: Observable<[BookmarkResponse]> = .just([]) + public var fetchItemBookmarkResult: Observable<[BookmarkResponse]> = .just([]) + public var fetchNPCBookmarkResult: Observable<[BookmarkResponse]> = .just([]) + public var fetchQuestBookmarkResult: Observable<[BookmarkResponse]> = .just([]) + public var fetchMapBookmarkResult: Observable<[BookmarkResponse]> = .just([]) + + public init() {} + + public func setBookmark(resourceId: Int, type: DictionaryItemType) -> Observable { + setBookmarkResult + } + + public func deleteBookmark(bookmarkId: Int) -> Observable { + deleteBookmarkResult + } + + public func fetchBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + fetchBookmarkResult + } + + public func fetchMonsterBookmark(minLevel: Int?, maxLevel: Int?, sort: String?) -> Observable<[BookmarkResponse]> { + fetchMonsterBookmarkResult + } + + public func fetchItemBookmark(jobId: Int?, minLevel: Int?, maxLevel: Int?, categoryIds: [Int]?, sort: String?) -> Observable<[BookmarkResponse]> { + fetchItemBookmarkResult + } + + public func fetchNPCBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + fetchNPCBookmarkResult + } + + public func fetchQuestBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + fetchQuestBookmarkResult + } + + public func fetchMapBookmark(sort: String?) -> Observable<[BookmarkResponse]> { + fetchMapBookmarkResult + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkUserDefaultsRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkUserDefaultsRepository.swift new file mode 100644 index 00000000..4a4e29f7 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockBookmarkUserDefaultsRepository.swift @@ -0,0 +1,17 @@ +import MLSBookmarkFeatureInterface +import RxSwift + +public final class MockBookmarkUserDefaultsRepository: BookmarkUserDefaultsRepository { + public var hasVisitedResult: Observable = .just(false) + public var markVisitedResult: Completable = .empty() + + public init() {} + + public func hasVisitedOnboarding() -> Observable { + hasVisitedResult + } + + public func markOnboardingVisited() -> Completable { + markVisitedResult + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockCollectionRepository.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockCollectionRepository.swift new file mode 100644 index 00000000..65a76757 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockCollectionRepository.swift @@ -0,0 +1,37 @@ +import MLSBookmarkFeatureInterface +import RxSwift + +public final class MockCollectionRepository: CollectionRepository { + public var fetchCollectionListResult: Observable<[CollectionResponse]> = .just([]) + public var createCollectionListResult: Completable = .empty() + public var fetchCollectionResult: Observable<[BookmarkResponse]> = .just([]) + public var updateCollectionNameResult: Completable = .empty() + public var deleteCollectionResult: Completable = .empty() + public var addCollectionAndBookmarkResult: Completable = .empty() + + public init() {} + + public func fetchCollectionList(sort: String?) -> Observable<[CollectionResponse]> { + fetchCollectionListResult + } + + public func createCollectionList(name: String) -> Completable { + createCollectionListResult + } + + public func fetchCollection(id: Int) -> Observable<[BookmarkResponse]> { + fetchCollectionResult + } + + public func updateCollectionName(collectionId: Int, name: String) -> Completable { + updateCollectionNameResult + } + + public func deleteCollection(collectionId: Int) -> Completable { + deleteCollectionResult + } + + public func addCollectionAndBookmark(collectionIds: [Int], bookmarkIds: [Int]) -> Completable { + addCollectionAndBookmarkResult + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockParseItemFilterResultUseCase.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockParseItemFilterResultUseCase.swift new file mode 100644 index 00000000..3dac15de --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/MockParseItemFilterResultUseCase.swift @@ -0,0 +1,11 @@ +import MLSBookmarkFeatureInterface + +public final class MockParseItemFilterResultUseCase: ParseItemFilterResultUseCase { + public var result: ItemFilterCriteria = ItemFilterCriteria(jobIds: [], startLevel: nil, endLevel: nil, categoryIds: []) + + public init() {} + + public func execute(results: [(String, String)]) -> ItemFilterCriteria { + result + } +} diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift new file mode 100644 index 00000000..28cf0860 --- /dev/null +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift @@ -0,0 +1,74 @@ +import MLSAuthFeatureInterface +import MLSBookmarkFeatureInterface +import MLSCore +import MLSDesignSystem +import RxRelay +import UIKit + +// MARK: - Stub Modal ViewController + +public final class StubModalViewController: BaseViewController, ModalPresentable { + public var modalHeight: CGFloat? = nil +} + +// MARK: - Login + +public final class StubLoginFactory: LoginFactory { + public init() {} + public func make(exitRoute: LoginExitRoute, onLoginCompleted: (() -> Void)?) -> BaseViewController { + BaseViewController() + } +} + +// MARK: - Dictionary External Factories + +public final class StubDictionarySearchFactory: DictionarySearchFactory { + public init() {} + public func make() -> BaseViewController { BaseViewController() } +} + +public final class StubDictionaryNotificationFactory: DictionaryNotificationFactory { + public init() {} + public func make() -> BaseViewController { BaseViewController() } +} + +public final class StubDictionaryDetailFactory: DictionaryDetailFactory { + public init() {} + public func make( + type: DictionaryType, + id: Int, + bookmarkRelay: PublishRelay<(id: Int, newBookmarkId: Int?)>?, + loginRelay: PublishRelay? + ) -> BaseViewController { + BaseViewController() + } +} + +public final class StubSortedBottomSheetFactory: SortedBottomSheetFactory { + public init() {} + public func make( + sortedOptions: [SortType], + selectedIndex: Int, + onSelectedIndex: @escaping (Int) -> Void + ) -> BaseViewController & ModalPresentable { + StubModalViewController() + } +} + +public final class StubItemFilterBottomSheetFactory: ItemFilterBottomSheetFactory { + public init() {} + public func make(onFilterSelected: @escaping ([(String, String)]) -> Void) -> BaseViewController { + BaseViewController() + } +} + +public final class StubMonsterFilterBottomSheetFactory: MonsterFilterBottomSheetFactory { + public init() {} + public func make( + startLevel: Int, + endLevel: Int, + onFilterSelected: @escaping (Int, Int) -> Void + ) -> BaseViewController & ModalPresentable { + StubModalViewController() + } +} diff --git a/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift b/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift new file mode 100644 index 00000000..1063516d --- /dev/null +++ b/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift @@ -0,0 +1,79 @@ +import MLSBookmarkFeatureInterface +import MLSBookmarkFeatureTesting +import RxBlocking +import XCTest +@testable import MLSBookmarkFeature + +final class BookmarkListReactorTests: XCTestCase { + var authRepository: MockBookmarkAuthRepository! + var bookmarkRepository: MockBookmarkRepository! + var parseUseCase: MockParseItemFilterResultUseCase! + + override func setUp() { + super.setUp() + authRepository = MockBookmarkAuthRepository() + bookmarkRepository = MockBookmarkRepository() + parseUseCase = MockParseItemFilterResultUseCase() + } + + func test_viewWillAppear_whenLoggedIn_fetchesItems() throws { + let expectedItems = [ + BookmarkResponse(name: "테스트", bookmarkId: 1, originalId: 100, imageUrl: nil, type: .item, level: nil) + ] + authRepository.isLoggedInResult = .just(true) + bookmarkRepository.fetchBookmarkResult = .just(expectedItems) + + let reactor = BookmarkListReactor( + type: .total, + authRepository: authRepository, + bookmarkRepository: bookmarkRepository, + parseItemFilterResultUseCase: parseUseCase + ) + + reactor.action.onNext(.viewWillAppear) + + let items = try reactor.state.map(\.items).toBlocking(timeout: 1).first() + XCTAssertEqual(items, expectedItems) + } + + func test_viewWillAppear_whenLoggedOut_itemsEmpty() throws { + authRepository.isLoggedInResult = .just(false) + + let reactor = BookmarkListReactor( + type: .total, + authRepository: authRepository, + bookmarkRepository: bookmarkRepository, + parseItemFilterResultUseCase: parseUseCase + ) + + reactor.action.onNext(.viewWillAppear) + + let isLogin = try reactor.state.map(\.isLogin).toBlocking(timeout: 1).first() + XCTAssertEqual(isLogin, false) + XCTAssertTrue(reactor.currentState.items.isEmpty) + } + + func test_toggleBookmark_removesItemAndEmitsDeleteEvent() throws { + let item = BookmarkResponse(name: "테스트", bookmarkId: 1, originalId: 100, imageUrl: nil, type: .item, level: nil) + authRepository.isLoggedInResult = .just(true) + bookmarkRepository.fetchBookmarkResult = .just([item]) + bookmarkRepository.deleteBookmarkResult = .empty() + + let reactor = BookmarkListReactor( + type: .total, + authRepository: authRepository, + bookmarkRepository: bookmarkRepository, + parseItemFilterResultUseCase: parseUseCase + ) + + reactor.action.onNext(.viewWillAppear) + reactor.action.onNext(.toggleBookmark(100)) + + let event = try reactor.state.map(\.uiEvent).toBlocking(timeout: 1).first() + if case .delete(let deletedItem) = event { + XCTAssertEqual(deletedItem.originalId, 100) + } else { + XCTFail("Expected delete event") + } + } +} From d05ecd10af54ecdb3c7927bd49651187c05dea36 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Fri, 29 May 2026 10:46:03 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat/#333:=20MLSBookmarkFeatureExample=20?= =?UTF-8?q?=EC=95=B1=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppDelegate.swift | 16 +++ .../Base.lproj/LaunchScreen.storyboard | 25 ++++ MLS/MLSBookmarkFeatureExample/Info.plist | 25 ++++ .../MenuViewController.swift | 131 ++++++++++++++++++ .../SceneDelegate.swift | 86 ++++++++++++ MLS/MLSBookmarkFeatureExample/Stubs.swift | 1 + 6 files changed, 284 insertions(+) create mode 100644 MLS/MLSBookmarkFeatureExample/AppDelegate.swift create mode 100644 MLS/MLSBookmarkFeatureExample/Base.lproj/LaunchScreen.storyboard create mode 100644 MLS/MLSBookmarkFeatureExample/Info.plist create mode 100644 MLS/MLSBookmarkFeatureExample/MenuViewController.swift create mode 100644 MLS/MLSBookmarkFeatureExample/SceneDelegate.swift create mode 100644 MLS/MLSBookmarkFeatureExample/Stubs.swift diff --git a/MLS/MLSBookmarkFeatureExample/AppDelegate.swift b/MLS/MLSBookmarkFeatureExample/AppDelegate.swift new file mode 100644 index 00000000..eb30b59f --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/AppDelegate.swift @@ -0,0 +1,16 @@ +import MLSDesignSystem +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + FontManager.registerFonts() + return true + } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {} +} diff --git a/MLS/MLSBookmarkFeatureExample/Base.lproj/LaunchScreen.storyboard b/MLS/MLSBookmarkFeatureExample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MLS/MLSBookmarkFeatureExample/Info.plist b/MLS/MLSBookmarkFeatureExample/Info.plist new file mode 100644 index 00000000..bc7411a1 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/Info.plist @@ -0,0 +1,25 @@ + + + + + UILaunchStoryboardName + LaunchScreen + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/MLS/MLSBookmarkFeatureExample/MenuViewController.swift b/MLS/MLSBookmarkFeatureExample/MenuViewController.swift new file mode 100644 index 00000000..2444ebc8 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/MenuViewController.swift @@ -0,0 +1,131 @@ +import MLSBookmarkFeature +import MLSBookmarkFeatureInterface +import MLSBookmarkFeatureTesting +import MLSCore +import UIKit + +final class MenuViewController: UITableViewController { + + private struct MenuItem { + let title: String + let subtitle: String + let action: (UINavigationController) -> Void + } + + private lazy var items: [MenuItem] = [ + MenuItem( + title: "BookmarkMain", + subtitle: "북마크 메인 (탭 + 페이지)" + ) { nav in + let factory = DIContainer.resolve(type: BookmarkMainFactory.self) + let vc = factory.make(bottomInset: 0) + let child = UINavigationController(rootViewController: vc) + child.navigationBar.isHidden = true + child.modalPresentationStyle = .fullScreen + nav.present(child, animated: true) + }, + MenuItem( + title: "BookmarkList – 전체", + subtitle: "전체 북마크 리스트" + ) { nav in + let factory = DIContainer.resolve(type: BookmarkListFactory.self) + let vc = factory.make(type: .total, listType: .bookmark) + nav.pushViewController(vc, animated: true) + }, + MenuItem( + title: "BookmarkList – 아이템", + subtitle: "아이템 북마크 리스트 (필터 포함)" + ) { nav in + let factory = DIContainer.resolve(type: BookmarkListFactory.self) + let vc = factory.make(type: .item, listType: .bookmark) + nav.pushViewController(vc, animated: true) + }, + MenuItem( + title: "BookmarkList – 몬스터", + subtitle: "몬스터 북마크 리스트 (필터 포함)" + ) { nav in + let factory = DIContainer.resolve(type: BookmarkListFactory.self) + let vc = factory.make(type: .monster, listType: .bookmark) + nav.pushViewController(vc, animated: true) + }, + MenuItem( + title: "CollectionList", + subtitle: "컬렉션 목록" + ) { nav in + let factory = DIContainer.resolve(type: CollectionListFactory.self) + let vc = factory.make() + nav.pushViewController(vc, animated: true) + }, + MenuItem( + title: "CollectionDetail", + subtitle: "컬렉션 상세 (빈 컬렉션)" + ) { nav in + let factory = DIContainer.resolve(type: CollectionDetailFactory.self) + let stub = CollectionResponse( + collectionId: 1, + name: "내 컬렉션", + createdAt: [2024, 1, 1], + recentBookmarks: [] + ) + let vc = factory.make(collection: stub, onMoveToMain: nil) + nav.pushViewController(vc, animated: true) + }, + MenuItem( + title: "BookmarkModal", + subtitle: "컬렉션에 북마크 추가 모달" + ) { nav in + let factory = DIContainer.resolve(type: BookmarkModalFactory.self) + let vc = factory.make(bookmarkIds: [1, 2, 3]) + vc.modalPresentationStyle = .pageSheet + if let sheet = vc.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + } + nav.present(vc, animated: true) + }, + MenuItem( + title: "AddCollection", + subtitle: "새 컬렉션 추가 모달" + ) { nav in + let factory = DIContainer.resolve(type: AddCollectionFactory.self) + let vc = factory.make(collection: nil) + nav.present(vc, animated: true) + }, + MenuItem( + title: "BookmarkOnBoarding", + subtitle: "온보딩 화면" + ) { nav in + let factory = DIContainer.resolve(type: BookmarkOnBoardingFactory.self) + let vc = factory.make() + vc.modalPresentationStyle = .fullScreen + nav.present(vc, animated: true) + }, + ] + + override func viewDidLoad() { + super.viewDidLoad() + title = "Bookmark Feature" + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + let item = items[indexPath.row] + var config = cell.defaultContentConfiguration() + config.text = item.title + config.secondaryText = item.subtitle + cell.contentConfiguration = config + cell.accessoryType = .disclosureIndicator + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let nav = navigationController else { return } + items[indexPath.row].action(nav) + } +} diff --git a/MLS/MLSBookmarkFeatureExample/SceneDelegate.swift b/MLS/MLSBookmarkFeatureExample/SceneDelegate.swift new file mode 100644 index 00000000..aefd3329 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/SceneDelegate.swift @@ -0,0 +1,86 @@ +import MLSBookmarkFeature +import MLSBookmarkFeatureInterface +import MLSBookmarkFeatureTesting +import MLSCore +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + registerDependencies() + + let window = UIWindow(windowScene: windowScene) + window.backgroundColor = .white + self.window = window + + let menuVC = MenuViewController() + let nav = UINavigationController(rootViewController: menuVC) + window.rootViewController = nav + window.makeKeyAndVisible() + } + + private func registerDependencies() { + let mockBookmarkRepository = MockBookmarkRepository() + let mockCollectionRepository = MockCollectionRepository() + let mockAuthRepository = MockBookmarkAuthRepository() + let mockUserDefaultsRepository = MockBookmarkUserDefaultsRepository() + let mockParseUseCase = MockParseItemFilterResultUseCase() + + let addCollectionFactory = AddCollectionFactoryImpl(collectionRepository: mockCollectionRepository) + let bookmarkModalFactory = BookmarkModalFactoryImpl( + addCollectionFactory: addCollectionFactory, + collectionRepository: mockCollectionRepository + ) + let collectionSettingFactory = CollectionSettingFactoryImpl() + let collectionEditFactory = CollectionEditFactoryImpl(bookmarkModalFactory: bookmarkModalFactory) + let collectionDetailFactory = CollectionDetailFactoryImpl( + bookmarkModalFactory: bookmarkModalFactory, + collectionSettingFactory: collectionSettingFactory, + addCollectionFactory: addCollectionFactory, + collectionEditFactory: collectionEditFactory, + dictionaryDetailFactory: StubDictionaryDetailFactory(), + collectionRepository: mockCollectionRepository + ) + let collectionListFactory = CollectionListFactoryImpl( + collectionRepository: mockCollectionRepository, + addCollectionFactory: addCollectionFactory, + collectionDetailFactory: collectionDetailFactory, + sortedBottomSheetFactory: StubSortedBottomSheetFactory() + ) + let bookmarkListFactory = BookmarkListFactoryImpl( + itemFilterFactory: StubItemFilterBottomSheetFactory(), + monsterFilterFactory: StubMonsterFilterBottomSheetFactory(), + sortedFactory: StubSortedBottomSheetFactory(), + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: StubLoginFactory(), + dictionaryDetailFactory: StubDictionaryDetailFactory(), + collectionEditFactory: collectionEditFactory, + authRepository: mockAuthRepository, + bookmarkRepository: mockBookmarkRepository, + parseItemFilterResultUseCase: mockParseUseCase + ) + let onBoardingFactory = BookmarkOnBoardingFactoryImpl() + + DIContainer.register(type: BookmarkMainFactory.self) { + BookmarkMainFactoryImpl( + authRepository: mockAuthRepository, + userDefaultsRepository: mockUserDefaultsRepository, + onBoardingFactory: onBoardingFactory, + bookmarkListFactory: bookmarkListFactory, + collectionListFactory: collectionListFactory, + searchFactory: StubDictionarySearchFactory(), + notificationFactory: StubDictionaryNotificationFactory(), + loginFactory: StubLoginFactory() + ) + } + DIContainer.register(type: BookmarkListFactory.self) { bookmarkListFactory } + DIContainer.register(type: CollectionListFactory.self) { collectionListFactory } + DIContainer.register(type: CollectionDetailFactory.self) { collectionDetailFactory } + DIContainer.register(type: BookmarkModalFactory.self) { bookmarkModalFactory } + DIContainer.register(type: AddCollectionFactory.self) { addCollectionFactory } + DIContainer.register(type: BookmarkOnBoardingFactory.self) { onBoardingFactory } + } +} diff --git a/MLS/MLSBookmarkFeatureExample/Stubs.swift b/MLS/MLSBookmarkFeatureExample/Stubs.swift new file mode 100644 index 00000000..c1474e27 --- /dev/null +++ b/MLS/MLSBookmarkFeatureExample/Stubs.swift @@ -0,0 +1 @@ +// Stubs are defined in MLSBookmarkFeatureTesting From 81bff073a1d8da481ea60b57bdf5b5e136637247 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Fri, 29 May 2026 10:46:06 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat/#333:=20Xcode=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=EC=97=90=20MLSBookmarkFeatureExample=20?= =?UTF-8?q?=ED=83=80=EA=B2=9F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLS.xcodeproj/project.pbxproj | 185 +++++++++++++++++++ MLS/MLS.xcworkspace/contents.xcworkspacedata | 3 + 2 files changed, 188 insertions(+) diff --git a/MLS/MLS.xcodeproj/project.pbxproj b/MLS/MLS.xcodeproj/project.pbxproj index 6c989cc7..f0c84c26 100644 --- a/MLS/MLS.xcodeproj/project.pbxproj +++ b/MLS/MLS.xcodeproj/project.pbxproj @@ -74,6 +74,12 @@ 77FA68BA2F72C9C10064B6EB /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 77FA68B92F72C9C10064B6EB /* RxSwift */; }; 77FA68BC2F72C9C70064B6EB /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 77FA68BB2F72C9C70064B6EB /* SnapKit */; }; 77FA68BE2F72CA490064B6EB /* MLSDesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 77FA68BD2F72CA490064B6EB /* MLSDesignSystem */; }; + BBCC0B002FA000000000BBCC /* MLSBookmarkFeature in Frameworks */ = {isa = PBXBuildFile; productRef = BBCC0C002FA000000000BBCC /* MLSBookmarkFeature */; }; + BBCC0D002FA000000000BBCC /* MLSBookmarkFeatureInterface in Frameworks */ = {isa = PBXBuildFile; productRef = BBCC0E002FA000000000BBCC /* MLSBookmarkFeatureInterface */; }; + BBCC0F002FA000000000BBCC /* MLSBookmarkFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = BBCC10002FA000000000BBCC /* MLSBookmarkFeatureTesting */; }; + BBCC11002FA000000000BBCC /* MLSCore in Frameworks */ = {isa = PBXBuildFile; productRef = BBCC12002FA000000000BBCC /* MLSCore */; }; + BBCC13002FA000000000BBCC /* MLSAuthFeatureInterface in Frameworks */ = {isa = PBXBuildFile; productRef = BBCC14002FA000000000BBCC /* MLSAuthFeatureInterface */; }; + BBCC15002FA000000000BBCC /* MLSDesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = BBCC16002FA000000000BBCC /* MLSDesignSystem */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -165,6 +171,7 @@ 77E260402EEABEC40059E889 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Settings.bundle; path = MLS/Resource/Settings.bundle; sourceTree = ""; }; 77EB18D52DED9256004FB380 /* AuthFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77FA687A2F72C7360064B6EB /* MLSDesignSystemExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSDesignSystemExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BBCC02002FA000000000BBCC /* MLSBookmarkFeatureExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSBookmarkFeatureExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -203,6 +210,13 @@ ); target = 77FA68792F72C7360064B6EB /* MLSDesignSystemExample */; }; + BBCC04002FA000000000BBCC /* Exceptions for "MLSBookmarkFeatureExample" folder in "MLSBookmarkFeatureExample" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = BBCC01002FA000000000BBCC /* MLSBookmarkFeatureExample */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -251,6 +265,14 @@ path = MLSDesignSystemExample; sourceTree = ""; }; + BBCC03002FA000000000BBCC /* MLSBookmarkFeatureExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + BBCC04002FA000000000BBCC /* Exceptions for "MLSBookmarkFeatureExample" folder in "MLSBookmarkFeatureExample" target */, + ); + path = MLSBookmarkFeatureExample; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -342,6 +364,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BBCC06002FA000000000BBCC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BBCC0B002FA000000000BBCC /* MLSBookmarkFeature in Frameworks */, + BBCC0D002FA000000000BBCC /* MLSBookmarkFeatureInterface in Frameworks */, + BBCC0F002FA000000000BBCC /* MLSBookmarkFeatureTesting in Frameworks */, + BBCC11002FA000000000BBCC /* MLSCore in Frameworks */, + BBCC13002FA000000000BBCC /* MLSAuthFeatureInterface in Frameworks */, + BBCC15002FA000000000BBCC /* MLSDesignSystem in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -428,6 +463,7 @@ 08F7A9242F86745C00EF5C06 /* MLSAuthFeatureExample */, 77217FBF2F9A04CF000915EF /* MLSMyPageFeatureExample */, 08F7DC622F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */, + BBCC03002FA000000000BBCC /* MLSBookmarkFeatureExample */, 084A25312DB93A5400C395C0 /* Frameworks */, 087D3EE92DA7972C002F924D /* Products */, ); @@ -442,6 +478,7 @@ 08F7A9232F86745C00EF5C06 /* MLSAuthFeatureExample.app */, 77217FBE2F9A04CF000915EF /* MLSMyPageFeatureExample.app */, 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */, + BBCC02002FA000000000BBCC /* MLSBookmarkFeatureExample.app */, ); name = Products; sourceTree = ""; @@ -613,6 +650,34 @@ productReference = 77FA687A2F72C7360064B6EB /* MLSDesignSystemExample.app */; productType = "com.apple.product-type.application"; }; + BBCC01002FA000000000BBCC /* MLSBookmarkFeatureExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = BBCC08002FA000000000BBCC /* Build configuration list for PBXNativeTarget "MLSBookmarkFeatureExample" */; + buildPhases = ( + BBCC05002FA000000000BBCC /* Sources */, + BBCC06002FA000000000BBCC /* Frameworks */, + BBCC07002FA000000000BBCC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + BBCC03002FA000000000BBCC /* MLSBookmarkFeatureExample */, + ); + name = MLSBookmarkFeatureExample; + packageProductDependencies = ( + BBCC0C002FA000000000BBCC /* MLSBookmarkFeature */, + BBCC0E002FA000000000BBCC /* MLSBookmarkFeatureInterface */, + BBCC10002FA000000000BBCC /* MLSBookmarkFeatureTesting */, + BBCC12002FA000000000BBCC /* MLSCore */, + BBCC14002FA000000000BBCC /* MLSAuthFeatureInterface */, + BBCC16002FA000000000BBCC /* MLSDesignSystem */, + ); + productName = MLSBookmarkFeatureExample; + productReference = BBCC02002FA000000000BBCC /* MLSBookmarkFeatureExample.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -699,6 +764,7 @@ 08F7A9222F86745C00EF5C06 /* MLSAuthFeatureExample */, 77217FBD2F9A04CF000915EF /* MLSMyPageFeatureExample */, 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */, + BBCC01002FA000000000BBCC /* MLSBookmarkFeatureExample */, ); }; /* End PBXProject section */ @@ -749,6 +815,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BBCC07002FA000000000BBCC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -815,6 +888,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BBCC05002FA000000000BBCC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1365,6 +1445,78 @@ }; name = Release; }; + BBCC09002FA000000000BBCC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MLSBookmarkFeatureExample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLS.BookmarkFeatureExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BBCC0A002FA000000000BBCC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MLSBookmarkFeatureExample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLS.BookmarkFeatureExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1431,6 +1583,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BBCC08002FA000000000BBCC /* Build configuration list for PBXNativeTarget "MLSBookmarkFeatureExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BBCC09002FA000000000BBCC /* Debug */, + BBCC0A002FA000000000BBCC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1619,6 +1780,30 @@ isa = XCSwiftPackageProductDependency; productName = MLSDesignSystem; }; + BBCC0C002FA000000000BBCC /* MLSBookmarkFeature */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSBookmarkFeature; + }; + BBCC0E002FA000000000BBCC /* MLSBookmarkFeatureInterface */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSBookmarkFeatureInterface; + }; + BBCC10002FA000000000BBCC /* MLSBookmarkFeatureTesting */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSBookmarkFeatureTesting; + }; + BBCC12002FA000000000BBCC /* MLSCore */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSCore; + }; + BBCC14002FA000000000BBCC /* MLSAuthFeatureInterface */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSAuthFeatureInterface; + }; + BBCC16002FA000000000BBCC /* MLSDesignSystem */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSDesignSystem; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 087D3EE02DA7972C002F924D /* Project object */; diff --git a/MLS/MLS.xcworkspace/contents.xcworkspacedata b/MLS/MLS.xcworkspace/contents.xcworkspacedata index c889074c..f78f1818 100644 --- a/MLS/MLS.xcworkspace/contents.xcworkspacedata +++ b/MLS/MLS.xcworkspace/contents.xcworkspacedata @@ -53,4 +53,7 @@ + + From 5728de151898899cfcec0d672236fcb08fe17f69 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 06:00:36 +0000 Subject: [PATCH 5/5] style/#333: Apply SwiftLint autocorrect --- .../Presentation/BookmarkList/BookmarkListReactor.swift | 4 +--- .../Sources/MLSBookmarkFeatureTesting/Stubs.swift | 2 +- .../MLSBookmarkFeatureTests/BookmarkListReactorTests.swift | 2 +- MLS/MLSBookmarkFeatureExample/MenuViewController.swift | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift index 9a59aa44..a0201188 100644 --- a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeature/Presentation/BookmarkList/BookmarkListReactor.swift @@ -69,9 +69,7 @@ final class BookmarkListReactor: Reactor { var endLevel: Int? var lastDeletedBookmark: BookmarkResponse? var viewState: ViewState { - if !isLogin { return .logout } - else if items.isEmpty { return .loginWithoutData } - else { return .loginWithData } + if !isLogin { return .logout } else if items.isEmpty { return .loginWithoutData } else { return .loginWithData } } } diff --git a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift index 28cf0860..81656be2 100644 --- a/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift +++ b/MLS/MLSBookmarkFeature/Sources/MLSBookmarkFeatureTesting/Stubs.swift @@ -8,7 +8,7 @@ import UIKit // MARK: - Stub Modal ViewController public final class StubModalViewController: BaseViewController, ModalPresentable { - public var modalHeight: CGFloat? = nil + public var modalHeight: CGFloat? } // MARK: - Login diff --git a/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift b/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift index 1063516d..a568d224 100644 --- a/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift +++ b/MLS/MLSBookmarkFeature/Tests/MLSBookmarkFeatureTests/BookmarkListReactorTests.swift @@ -1,8 +1,8 @@ +@testable import MLSBookmarkFeature import MLSBookmarkFeatureInterface import MLSBookmarkFeatureTesting import RxBlocking import XCTest -@testable import MLSBookmarkFeature final class BookmarkListReactorTests: XCTestCase { var authRepository: MockBookmarkAuthRepository! diff --git a/MLS/MLSBookmarkFeatureExample/MenuViewController.swift b/MLS/MLSBookmarkFeatureExample/MenuViewController.swift index 2444ebc8..0a0f7e58 100644 --- a/MLS/MLSBookmarkFeatureExample/MenuViewController.swift +++ b/MLS/MLSBookmarkFeatureExample/MenuViewController.swift @@ -99,7 +99,7 @@ final class MenuViewController: UITableViewController { let vc = factory.make() vc.modalPresentationStyle = .fullScreen nav.present(vc, animated: true) - }, + } ] override func viewDidLoad() {