diff --git a/SOOUM/Podfile b/SOOUM/Podfile index 91759f34..c43eb8f7 100644 --- a/SOOUM/Podfile +++ b/SOOUM/Podfile @@ -13,9 +13,9 @@ def pods pod 'Alamofire', '~> 5.9.1' - pod 'Firebase/Analytics', '~> 10.22.0' - pod 'Firebase/Crashlytics', '~> 10.22.0' - pod 'Firebase/Messaging', '~> 10.22.0' + pod 'Firebase/Analytics', '~> 10.24.0' + pod 'Firebase/Crashlytics', '~> 10.24.0' + pod 'Firebase/Messaging', '~> 10.24.0' pod 'Clarity' diff --git a/SOOUM/Podfile.lock b/SOOUM/Podfile.lock index 29b8109f..c71ed25d 100644 --- a/SOOUM/Podfile.lock +++ b/SOOUM/Podfile.lock @@ -1,24 +1,24 @@ PODS: - Alamofire (5.9.1) - - Clarity (3.0.5) + - Clarity (3.3.5) - CocoaLumberjack/Core (3.7.4) - CocoaLumberjack/Swift (3.7.4): - CocoaLumberjack/Core - - Firebase/Analytics (10.22.0): + - Firebase/Analytics (10.24.0): - Firebase/Core - - Firebase/Core (10.22.0): + - Firebase/Core (10.24.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.22.0) - - Firebase/CoreOnly (10.22.0): - - FirebaseCore (= 10.22.0) - - Firebase/Crashlytics (10.22.0): + - FirebaseAnalytics (~> 10.24.0) + - Firebase/CoreOnly (10.24.0): + - FirebaseCore (= 10.24.0) + - Firebase/Crashlytics (10.24.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 10.22.0) - - Firebase/Messaging (10.22.0): + - FirebaseCrashlytics (~> 10.24.0) + - Firebase/Messaging (10.24.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.22.0) - - FirebaseAnalytics (10.22.0): - - FirebaseAnalytics/AdIdSupport (= 10.22.0) + - FirebaseMessaging (~> 10.24.0) + - FirebaseAnalytics (10.24.0): + - FirebaseAnalytics/AdIdSupport (= 10.24.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -26,16 +26,16 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.22.0): + - FirebaseAnalytics/AdIdSupport (10.24.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.22.0) + - GoogleAppMeasurement (= 10.24.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseCore (10.22.0): + - FirebaseCore (10.24.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) @@ -43,9 +43,10 @@ PODS: - FirebaseCore (~> 10.0) - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.22.0): + - FirebaseCrashlytics (10.24.0): - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) + - FirebaseRemoteConfigInterop (~> 10.23) - FirebaseSessions (~> 10.5) - GoogleDataTransport (~> 9.2) - GoogleUtilities/Environment (~> 7.8) @@ -56,7 +57,7 @@ PODS: - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.22.0): + - FirebaseMessaging (10.24.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.3) @@ -65,6 +66,7 @@ PODS: - GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseRemoteConfigInterop (10.29.0) - FirebaseSessions (10.29.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) @@ -74,21 +76,21 @@ PODS: - GoogleUtilities/UserDefaults (~> 7.13) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - - GoogleAppMeasurement (10.22.0): - - GoogleAppMeasurement/AdIdSupport (= 10.22.0) + - GoogleAppMeasurement (10.24.0): + - GoogleAppMeasurement/AdIdSupport (= 10.24.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.22.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.22.0) + - GoogleAppMeasurement/AdIdSupport (10.24.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.24.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.22.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.24.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) @@ -127,7 +129,7 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/Privacy - Kingfisher (7.10.2) - - lottie-ios (4.5.1) + - lottie-ios (4.5.2) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) - nanopb/encode (= 2.30910.0) @@ -166,9 +168,9 @@ DEPENDENCIES: - Alamofire (~> 5.9.1) - Clarity - CocoaLumberjack/Swift (~> 3.7.2) - - Firebase/Analytics (~> 10.22.0) - - Firebase/Crashlytics (~> 10.22.0) - - Firebase/Messaging (~> 10.22.0) + - Firebase/Analytics (~> 10.24.0) + - Firebase/Crashlytics (~> 10.24.0) + - Firebase/Messaging (~> 10.24.0) - Kingfisher (~> 7.10.0) - lottie-ios - ReactorKit (~> 3.2.0) @@ -195,6 +197,7 @@ SPEC REPOS: - FirebaseCrashlytics - FirebaseInstallations - FirebaseMessaging + - FirebaseRemoteConfigInterop - FirebaseSessions - GoogleAppMeasurement - GoogleDataTransport @@ -221,22 +224,23 @@ SPEC REPOS: SPEC CHECKSUMS: Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c - Clarity: 214d0e426d63a8f6edbcd2a39edb5d2955728e0e + Clarity: fbd41ffa7b3c925ffb092a717c2dd896c39eff72 CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646 - Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71 - FirebaseAnalytics: 8d0ff929c63b7f72260f332b86ccf569776b75d3 - FirebaseCore: 0326ec9b05fbed8f8716cddbf0e36894a13837f7 + Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 + FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13 + FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 - FirebaseCrashlytics: e568d68ce89117c80cddb04073ab9018725fbb8c + FirebaseCrashlytics: af38ea4adfa606f6e63fcc22091b61e7938fcf66 FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd - FirebaseMessaging: 9f71037fd9db3376a4caa54e5a3949d1027b4b6e + FirebaseMessaging: 4d52717dd820707cc4eadec5eb981b4832ec8d5d + FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc - GoogleAppMeasurement: ccefe3eac9b0aa27f96066809fb1a7fe4b462626 + GoogleAppMeasurement: f3abf08495ef2cba7829f15318c373b8d9226491 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 Kingfisher: 99edc495d3b7607e6425f0d6f6847b2abd6d716d - lottie-ios: 248b380fa1b97d18e792c37d90da7ab2aa0d6562 + lottie-ios: 96784afc26ea031d3e2b6cae342a4b8915072489 nanopb: 438bc412db1928dac798aa6fd75726007be04262 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 @@ -255,6 +259,6 @@ SPEC CHECKSUMS: WeakMapTable: 05c694ce8439a7a9ebabb56187287a63c57673d6 YPImagePicker: afc81b3cffab05a6e7261c5daf80dc31b4e917b4 -PODFILE CHECKSUM: f064785e258ff9ada0428a59015c8a3fd3ccfe5d +PODFILE CHECKSUM: 2d750402f8449e78cdaa64307593cf3736124c1f COCOAPODS: 1.16.2 diff --git a/SOOUM/SOOUM.xcodeproj/project.pbxproj b/SOOUM/SOOUM.xcodeproj/project.pbxproj index 4478c271..7116e355 100644 --- a/SOOUM/SOOUM.xcodeproj/project.pbxproj +++ b/SOOUM/SOOUM.xcodeproj/project.pbxproj @@ -91,10 +91,6 @@ 380F42312E884FBC009AC59E /* CardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F422F2E884FB5009AC59E /* CardRepository.swift */; }; 380F42332E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */; }; 380F42342E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */; }; - 380F42362E885032009AC59E /* CardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42352E88502F009AC59E /* CardUseCase.swift */; }; - 380F42372E885032009AC59E /* CardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42352E88502F009AC59E /* CardUseCase.swift */; }; - 380F42392E88505B009AC59E /* CardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42382E885057009AC59E /* CardUseCaseImpl.swift */; }; - 380F423A2E88505B009AC59E /* CardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42382E885057009AC59E /* CardUseCaseImpl.swift */; }; 38121E312CA6C77500602499 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38121E302CA6C77500602499 /* Double.swift */; }; 38121E322CA6C77500602499 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38121E302CA6C77500602499 /* Double.swift */; }; 38121E342CA6DA4000602499 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38121E332CA6DA4000602499 /* Date.swift */; }; @@ -412,10 +408,6 @@ 3889A25D2E79BB340030F7CA /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A25B2E79BB2F0030F7CA /* UserRepository.swift */; }; 3889A2622E79BB5B0030F7CA /* UserRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A2612E79BB540030F7CA /* UserRepositoryImpl.swift */; }; 3889A2632E79BB5B0030F7CA /* UserRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A2612E79BB540030F7CA /* UserRepositoryImpl.swift */; }; - 3889A2652E79BBC40030F7CA /* UserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A2642E79BBC00030F7CA /* UserUseCase.swift */; }; - 3889A2662E79BBC40030F7CA /* UserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A2642E79BBC00030F7CA /* UserUseCase.swift */; }; - 3889A2682E79BC880030F7CA /* UserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A2672E79BC840030F7CA /* UserUseCaseImpl.swift */; }; - 3889A2692E79BC880030F7CA /* UserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A2672E79BC840030F7CA /* UserUseCaseImpl.swift */; }; 3889A26B2E79BD450030F7CA /* AuthRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A26A2E79BD410030F7CA /* AuthRemoteDataSource.swift */; }; 3889A26C2E79BD450030F7CA /* AuthRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A26A2E79BD410030F7CA /* AuthRemoteDataSource.swift */; }; 3889A26E2E79BE9F0030F7CA /* AuthRemoteDataSourceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3889A26D2E79BE970030F7CA /* AuthRemoteDataSourceImpl.swift */; }; @@ -470,6 +462,50 @@ 3894EDE42ED4B2BB0024213E /* FavoriteTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3894EDE22ED4B2BA0024213E /* FavoriteTagsViewModel.swift */; }; 389681102CAFBD6A00FFD89F /* DetailViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3896810F2CAFBD6A00FFD89F /* DetailViewReactor.swift */; }; 389681112CAFBD6A00FFD89F /* DetailViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3896810F2CAFBD6A00FFD89F /* DetailViewReactor.swift */; }; + 389E59A52EDEE39500D0946D /* ValidateUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59A42EDEE38E00D0946D /* ValidateUserUseCase.swift */; }; + 389E59A62EDEE39500D0946D /* ValidateUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59A42EDEE38E00D0946D /* ValidateUserUseCase.swift */; }; + 389E59A82EDEE6F600D0946D /* ValidateNicknameUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59A72EDEE6EF00D0946D /* ValidateNicknameUseCase.swift */; }; + 389E59A92EDEE6F600D0946D /* ValidateNicknameUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59A72EDEE6EF00D0946D /* ValidateNicknameUseCase.swift */; }; + 389E59AB2EDEE74B00D0946D /* UploadUserImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59AA2EDEE73500D0946D /* UploadUserImageUseCase.swift */; }; + 389E59AC2EDEE74B00D0946D /* UploadUserImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59AA2EDEE73500D0946D /* UploadUserImageUseCase.swift */; }; + 389E59AE2EDEE8BD00D0946D /* FetchCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59AD2EDEE8B500D0946D /* FetchCardUseCase.swift */; }; + 389E59AF2EDEE8BD00D0946D /* FetchCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59AD2EDEE8B500D0946D /* FetchCardUseCase.swift */; }; + 389E59B12EDEEA3A00D0946D /* FetchNoticeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B02EDEEA3300D0946D /* FetchNoticeUseCase.swift */; }; + 389E59B22EDEEA3A00D0946D /* FetchNoticeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B02EDEEA3300D0946D /* FetchNoticeUseCase.swift */; }; + 389E59B42EDEEA8200D0946D /* FetchCardDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B32EDEEA7600D0946D /* FetchCardDetailUseCase.swift */; }; + 389E59B52EDEEA8200D0946D /* FetchCardDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B32EDEEA7600D0946D /* FetchCardDetailUseCase.swift */; }; + 389E59B72EDEEAFC00D0946D /* UpdateCardLikeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B62EDEEAEB00D0946D /* UpdateCardLikeUseCase.swift */; }; + 389E59B82EDEEAFC00D0946D /* UpdateCardLikeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B62EDEEAEB00D0946D /* UpdateCardLikeUseCase.swift */; }; + 389E59BA2EDEEB8300D0946D /* BlockUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B92EDEEB8000D0946D /* BlockUserUseCase.swift */; }; + 389E59BB2EDEEB8300D0946D /* BlockUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59B92EDEEB8000D0946D /* BlockUserUseCase.swift */; }; + 389E59BD2EDEEBF300D0946D /* ReportCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59BC2EDEEBEA00D0946D /* ReportCardUseCase.swift */; }; + 389E59BE2EDEEBF300D0946D /* ReportCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59BC2EDEEBEA00D0946D /* ReportCardUseCase.swift */; }; + 389E59C02EDEEC4900D0946D /* DeleteCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59BF2EDEEC4500D0946D /* DeleteCardUseCase.swift */; }; + 389E59C12EDEEC4900D0946D /* DeleteCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59BF2EDEEC4500D0946D /* DeleteCardUseCase.swift */; }; + 389E59C32EDEEC7B00D0946D /* CardImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59C22EDEEC7600D0946D /* CardImageUseCase.swift */; }; + 389E59C42EDEEC7B00D0946D /* CardImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59C22EDEEC7600D0946D /* CardImageUseCase.swift */; }; + 389E59C62EDEECBF00D0946D /* WriteCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59C52EDEECBC00D0946D /* WriteCardUseCase.swift */; }; + 389E59C72EDEECBF00D0946D /* WriteCardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59C52EDEECBC00D0946D /* WriteCardUseCase.swift */; }; + 389E59C92EDEED2B00D0946D /* FetchTagUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59C82EDEED2700D0946D /* FetchTagUseCase.swift */; }; + 389E59CA2EDEED2B00D0946D /* FetchTagUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59C82EDEED2700D0946D /* FetchTagUseCase.swift */; }; + 389E59CC2EDEED6500D0946D /* FetchUserInfoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59CB2EDEED6100D0946D /* FetchUserInfoUseCase.swift */; }; + 389E59CD2EDEED6500D0946D /* FetchUserInfoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59CB2EDEED6100D0946D /* FetchUserInfoUseCase.swift */; }; + 389E59CF2EDEEDD500D0946D /* FetchFollowUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59CE2EDEEDD000D0946D /* FetchFollowUseCase.swift */; }; + 389E59D02EDEEDD500D0946D /* FetchFollowUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59CE2EDEEDD000D0946D /* FetchFollowUseCase.swift */; }; + 389E59D22EDEEE4100D0946D /* UpdateFollowUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59D12EDEEE3800D0946D /* UpdateFollowUseCase.swift */; }; + 389E59D32EDEEE4100D0946D /* UpdateFollowUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59D12EDEEE3800D0946D /* UpdateFollowUseCase.swift */; }; + 389E59D52EDEEE6B00D0946D /* UpdateUserInfoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59D42EDEEE6500D0946D /* UpdateUserInfoUseCase.swift */; }; + 389E59D62EDEEE6B00D0946D /* UpdateUserInfoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59D42EDEEE6500D0946D /* UpdateUserInfoUseCase.swift */; }; + 389E59D82EDEEEC500D0946D /* UpdateNotifyUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59D72EDEEEB900D0946D /* UpdateNotifyUseCase.swift */; }; + 389E59D92EDEEEC500D0946D /* UpdateNotifyUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59D72EDEEEB900D0946D /* UpdateNotifyUseCase.swift */; }; + 389E59DB2EDEEF3600D0946D /* FetchBlockUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59DA2EDEEF3000D0946D /* FetchBlockUserUseCase.swift */; }; + 389E59DC2EDEEF3600D0946D /* FetchBlockUserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59DA2EDEEF3000D0946D /* FetchBlockUserUseCase.swift */; }; + 389E59DE2EDEEF7C00D0946D /* TransferAccountUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59DD2EDEEF7600D0946D /* TransferAccountUseCase.swift */; }; + 389E59DF2EDEEF7C00D0946D /* TransferAccountUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59DD2EDEEF7600D0946D /* TransferAccountUseCase.swift */; }; + 389E59E12EDEEFA500D0946D /* UpdateTagFavoriteUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59E02EDEEF9E00D0946D /* UpdateTagFavoriteUseCase.swift */; }; + 389E59E22EDEEFA500D0946D /* UpdateTagFavoriteUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59E02EDEEF9E00D0946D /* UpdateTagFavoriteUseCase.swift */; }; + 389E59E42EDEF03000D0946D /* LocationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59E32EDEF02900D0946D /* LocationUseCase.swift */; }; + 389E59E52EDEF03000D0946D /* LocationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389E59E32EDEF02900D0946D /* LocationUseCase.swift */; }; 389EF8172D2F450000E053AE /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389EF8162D2F450000E053AE /* Log.swift */; }; 389EF8182D2F450000E053AE /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389EF8162D2F450000E053AE /* Log.swift */; }; 389EF81A2D2F454600E053AE /* Log+Extract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389EF8192D2F454600E053AE /* Log+Extract.swift */; }; @@ -496,6 +532,50 @@ 38AE77DC2E745FFF00B6FD13 /* EnterMemberTransferTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE77DA2E745FF700B6FD13 /* EnterMemberTransferTextFieldView.swift */; }; 38AE77DE2E7465F500B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE77DD2E7465E600B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift */; }; 38AE77DF2E7465F500B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE77DD2E7465E600B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift */; }; + 38AE85092EDF414400029E4C /* BlockUserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85082EDF413C00029E4C /* BlockUserUseCaseImpl.swift */; }; + 38AE850A2EDF414400029E4C /* BlockUserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85082EDF413C00029E4C /* BlockUserUseCaseImpl.swift */; }; + 38AE850C2EDF41B700029E4C /* CardImageUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE850B2EDF41AF00029E4C /* CardImageUseCaseImpl.swift */; }; + 38AE850D2EDF41B700029E4C /* CardImageUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE850B2EDF41AF00029E4C /* CardImageUseCaseImpl.swift */; }; + 38AE850F2EDF420700029E4C /* DeleteCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE850E2EDF420300029E4C /* DeleteCardUseCaseImpl.swift */; }; + 38AE85102EDF420700029E4C /* DeleteCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE850E2EDF420300029E4C /* DeleteCardUseCaseImpl.swift */; }; + 38AE85122EDF424800029E4C /* FetchBlockUserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85112EDF424600029E4C /* FetchBlockUserUseCaseImpl.swift */; }; + 38AE85132EDF424800029E4C /* FetchBlockUserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85112EDF424600029E4C /* FetchBlockUserUseCaseImpl.swift */; }; + 38AE85152EDF42B700029E4C /* FetchCardDetailUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85142EDF42B400029E4C /* FetchCardDetailUseCaseImpl.swift */; }; + 38AE85162EDF42B700029E4C /* FetchCardDetailUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85142EDF42B400029E4C /* FetchCardDetailUseCaseImpl.swift */; }; + 38AE85182EDF437400029E4C /* FetchCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85172EDF436F00029E4C /* FetchCardUseCaseImpl.swift */; }; + 38AE85192EDF437400029E4C /* FetchCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85172EDF436F00029E4C /* FetchCardUseCaseImpl.swift */; }; + 38AE851B2EDFF7E000029E4C /* FetchFollowUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE851A2EDFF7DE00029E4C /* FetchFollowUseCaseImpl.swift */; }; + 38AE851C2EDFF7E000029E4C /* FetchFollowUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE851A2EDFF7DE00029E4C /* FetchFollowUseCaseImpl.swift */; }; + 38AE851E2EDFF84700029E4C /* FetchNoticeUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE851D2EDFF84500029E4C /* FetchNoticeUseCaseImpl.swift */; }; + 38AE851F2EDFF84700029E4C /* FetchNoticeUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE851D2EDFF84500029E4C /* FetchNoticeUseCaseImpl.swift */; }; + 38AE85212EDFF88C00029E4C /* FetchTagUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85202EDFF88A00029E4C /* FetchTagUseCaseImpl.swift */; }; + 38AE85222EDFF88C00029E4C /* FetchTagUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85202EDFF88A00029E4C /* FetchTagUseCaseImpl.swift */; }; + 38AE85242EDFF90400029E4C /* FetchUserInfoUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85232EDFF90000029E4C /* FetchUserInfoUseCaseImpl.swift */; }; + 38AE85252EDFF90400029E4C /* FetchUserInfoUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85232EDFF90000029E4C /* FetchUserInfoUseCaseImpl.swift */; }; + 38AE85272EDFF95500029E4C /* LocationUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85262EDFF95200029E4C /* LocationUseCaseImpl.swift */; }; + 38AE85282EDFF95500029E4C /* LocationUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85262EDFF95200029E4C /* LocationUseCaseImpl.swift */; }; + 38AE852A2EDFF9CC00029E4C /* ReportCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85292EDFF99B00029E4C /* ReportCardUseCaseImpl.swift */; }; + 38AE852B2EDFF9CC00029E4C /* ReportCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85292EDFF99B00029E4C /* ReportCardUseCaseImpl.swift */; }; + 38AE852D2EDFFA3C00029E4C /* TransferAccountUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE852C2EDFFA3900029E4C /* TransferAccountUseCaseImpl.swift */; }; + 38AE852E2EDFFA3C00029E4C /* TransferAccountUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE852C2EDFFA3900029E4C /* TransferAccountUseCaseImpl.swift */; }; + 38AE85302EDFFA9500029E4C /* UpdateCardLikeUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE852F2EDFFA9300029E4C /* UpdateCardLikeUseCaseImpl.swift */; }; + 38AE85312EDFFA9500029E4C /* UpdateCardLikeUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE852F2EDFFA9300029E4C /* UpdateCardLikeUseCaseImpl.swift */; }; + 38AE85332EDFFAC400029E4C /* UpdateFollowUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85322EDFFAC300029E4C /* UpdateFollowUseCaseImpl.swift */; }; + 38AE85342EDFFAC400029E4C /* UpdateFollowUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85322EDFFAC300029E4C /* UpdateFollowUseCaseImpl.swift */; }; + 38AE85362EDFFAF900029E4C /* UpdateNotifyUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85352EDFFAF700029E4C /* UpdateNotifyUseCaseImpl.swift */; }; + 38AE85372EDFFAF900029E4C /* UpdateNotifyUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85352EDFFAF700029E4C /* UpdateNotifyUseCaseImpl.swift */; }; + 38AE85392EDFFBBF00029E4C /* UpdateTagFavoriteUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85382EDFFBBE00029E4C /* UpdateTagFavoriteUseCaseImpl.swift */; }; + 38AE853A2EDFFBBF00029E4C /* UpdateTagFavoriteUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85382EDFFBBE00029E4C /* UpdateTagFavoriteUseCaseImpl.swift */; }; + 38AE853C2EDFFBF700029E4C /* UpdateUserInfoUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE853B2EDFFBF500029E4C /* UpdateUserInfoUseCaseImpl.swift */; }; + 38AE853D2EDFFBF700029E4C /* UpdateUserInfoUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE853B2EDFFBF500029E4C /* UpdateUserInfoUseCaseImpl.swift */; }; + 38AE853F2EDFFC3600029E4C /* UploadUserImageUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE853E2EDFFC3500029E4C /* UploadUserImageUseCaseImpl.swift */; }; + 38AE85402EDFFC3600029E4C /* UploadUserImageUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE853E2EDFFC3500029E4C /* UploadUserImageUseCaseImpl.swift */; }; + 38AE85422EDFFCA600029E4C /* ValidateNicknameUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85412EDFFCA400029E4C /* ValidateNicknameUseCaseImpl.swift */; }; + 38AE85432EDFFCA600029E4C /* ValidateNicknameUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85412EDFFCA400029E4C /* ValidateNicknameUseCaseImpl.swift */; }; + 38AE85452EDFFCF900029E4C /* ValidateUserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85442EDFFCF800029E4C /* ValidateUserUseCaseImpl.swift */; }; + 38AE85462EDFFCF900029E4C /* ValidateUserUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85442EDFFCF800029E4C /* ValidateUserUseCaseImpl.swift */; }; + 38AE85482EDFFD7E00029E4C /* WriteCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85472EDFFD7B00029E4C /* WriteCardUseCaseImpl.swift */; }; + 38AE85492EDFFD7E00029E4C /* WriteCardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AE85472EDFFD7B00029E4C /* WriteCardUseCaseImpl.swift */; }; 38B21C022ECEF46200990F49 /* FavoriteTagViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B21C012ECEF45D00990F49 /* FavoriteTagViewModel.swift */; }; 38B21C032ECEF46200990F49 /* FavoriteTagViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B21C012ECEF45D00990F49 /* FavoriteTagViewModel.swift */; }; 38B21C082ECEF7D400990F49 /* PopularTagViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B21C072ECEF7CF00990F49 /* PopularTagViewCell.swift */; }; @@ -556,10 +636,6 @@ 38C2A7EB2EC074A200B941A2 /* SettingsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7E92EC0749A00B941A2 /* SettingsRepository.swift */; }; 38C2A7ED2EC074B200B941A2 /* SettingsRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7EC2EC074AE00B941A2 /* SettingsRepositoryImpl.swift */; }; 38C2A7EE2EC074B200B941A2 /* SettingsRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7EC2EC074AE00B941A2 /* SettingsRepositoryImpl.swift */; }; - 38C2A7F02EC0796700B941A2 /* SettingsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7EF2EC0795F00B941A2 /* SettingsUseCase.swift */; }; - 38C2A7F12EC0796700B941A2 /* SettingsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7EF2EC0795F00B941A2 /* SettingsUseCase.swift */; }; - 38C2A7F32EC0798B00B941A2 /* SettingsUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7F22EC0798600B941A2 /* SettingsUseCaseImpl.swift */; }; - 38C2A7F42EC0798B00B941A2 /* SettingsUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7F22EC0798600B941A2 /* SettingsUseCaseImpl.swift */; }; 38C2A7F62EC08FF600B941A2 /* BlockUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7F52EC08FEF00B941A2 /* BlockUserInfo.swift */; }; 38C2A7F72EC08FF600B941A2 /* BlockUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7F52EC08FEF00B941A2 /* BlockUserInfo.swift */; }; 38C2A7F92EC090B100B941A2 /* BlockUsersInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2A7F82EC090AC00B941A2 /* BlockUsersInfoResponse.swift */; }; @@ -598,10 +674,6 @@ 38C9AF212E9669F600B401C0 /* TagRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF1F2E9669F100B401C0 /* TagRepository.swift */; }; 38C9AF232E966A1B00B401C0 /* TagRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF222E966A1300B401C0 /* TagRepositoryImpl.swift */; }; 38C9AF242E966A1B00B401C0 /* TagRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF222E966A1300B401C0 /* TagRepositoryImpl.swift */; }; - 38C9AF262E966A6300B401C0 /* TagUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF252E966A6100B401C0 /* TagUseCase.swift */; }; - 38C9AF272E966A6300B401C0 /* TagUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF252E966A6100B401C0 /* TagUseCase.swift */; }; - 38C9AF292E966A8F00B401C0 /* TagUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF282E966A8B00B401C0 /* TagUseCaseImpl.swift */; }; - 38C9AF2A2E966A8F00B401C0 /* TagUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF282E966A8B00B401C0 /* TagUseCaseImpl.swift */; }; 38C9AF2D2E96A3E500B401C0 /* WriteCardTagModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF2C2E96A3D600B401C0 /* WriteCardTagModel.swift */; }; 38C9AF2E2E96A3E500B401C0 /* WriteCardTagModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF2C2E96A3D600B401C0 /* WriteCardTagModel.swift */; }; 38C9AF302E96A49F00B401C0 /* WriteCardTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C9AF2F2E96A49B00B401C0 /* WriteCardTag.swift */; }; @@ -718,6 +790,10 @@ 38F161482ECDA8F0003BADB6 /* FavoriteTagPlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F161462ECDA8D1003BADB6 /* FavoriteTagPlaceholderViewCell.swift */; }; 38F1614A2ECDAD34003BADB6 /* FavoriteTagViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F161492ECDAD29003BADB6 /* FavoriteTagViewCell.swift */; }; 38F1614B2ECDAD34003BADB6 /* FavoriteTagViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F161492ECDAD29003BADB6 /* FavoriteTagViewCell.swift */; }; + 38F3398F2EE31C870066A5F7 /* IsCardDeletedResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3398E2EE31C7E0066A5F7 /* IsCardDeletedResponse.swift */; }; + 38F339902EE31C870066A5F7 /* IsCardDeletedResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3398E2EE31C7E0066A5F7 /* IsCardDeletedResponse.swift */; }; + 38F339922EE328750066A5F7 /* WriteCardGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F339912EE328710066A5F7 /* WriteCardGuideView.swift */; }; + 38F339932EE328750066A5F7 /* WriteCardGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F339912EE328710066A5F7 /* WriteCardGuideView.swift */; }; 38F3760A2ECB772A00E4A41D /* FavoriteTagInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F376092ECB772600E4A41D /* FavoriteTagInfo.swift */; }; 38F3760B2ECB772A00E4A41D /* FavoriteTagInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F376092ECB772600E4A41D /* FavoriteTagInfo.swift */; }; 38F3760D2ECB779E00E4A41D /* FavoriteTagInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3760C2ECB778C00E4A41D /* FavoriteTagInfoResponse.swift */; }; @@ -817,8 +893,6 @@ 380F422C2E884F35009AC59E /* CardRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRemoteDataSourceImpl.swift; sourceTree = ""; }; 380F422F2E884FB5009AC59E /* CardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepository.swift; sourceTree = ""; }; 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepositoryImpl.swift; sourceTree = ""; }; - 380F42352E88502F009AC59E /* CardUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardUseCase.swift; sourceTree = ""; }; - 380F42382E885057009AC59E /* CardUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardUseCaseImpl.swift; sourceTree = ""; }; 38121E302CA6C77500602499 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 38121E332CA6DA4000602499 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 3816C05F2CCDE35300C8688C /* ErrorInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorInterceptor.swift; sourceTree = ""; }; @@ -986,8 +1060,6 @@ 3889A2552E79BA0F0030F7CA /* UserRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRemoteDataSourceImpl.swift; sourceTree = ""; }; 3889A25B2E79BB2F0030F7CA /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = ""; }; 3889A2612E79BB540030F7CA /* UserRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryImpl.swift; sourceTree = ""; }; - 3889A2642E79BBC00030F7CA /* UserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUseCase.swift; sourceTree = ""; }; - 3889A2672E79BC840030F7CA /* UserUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUseCaseImpl.swift; sourceTree = ""; }; 3889A26A2E79BD410030F7CA /* AuthRemoteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRemoteDataSource.swift; sourceTree = ""; }; 3889A26D2E79BE970030F7CA /* AuthRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRemoteDataSourceImpl.swift; sourceTree = ""; }; 3889A2702E79C0370030F7CA /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRepository.swift; sourceTree = ""; }; @@ -1016,6 +1088,28 @@ 3893B6D02D36739500F2004C /* CompositeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeManager.swift; sourceTree = ""; }; 3894EDE22ED4B2BA0024213E /* FavoriteTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagsViewModel.swift; sourceTree = ""; }; 3896810F2CAFBD6A00FFD89F /* DetailViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewReactor.swift; sourceTree = ""; }; + 389E59A42EDEE38E00D0946D /* ValidateUserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateUserUseCase.swift; sourceTree = ""; }; + 389E59A72EDEE6EF00D0946D /* ValidateNicknameUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateNicknameUseCase.swift; sourceTree = ""; }; + 389E59AA2EDEE73500D0946D /* UploadUserImageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadUserImageUseCase.swift; sourceTree = ""; }; + 389E59AD2EDEE8B500D0946D /* FetchCardUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCardUseCase.swift; sourceTree = ""; }; + 389E59B02EDEEA3300D0946D /* FetchNoticeUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchNoticeUseCase.swift; sourceTree = ""; }; + 389E59B32EDEEA7600D0946D /* FetchCardDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCardDetailUseCase.swift; sourceTree = ""; }; + 389E59B62EDEEAEB00D0946D /* UpdateCardLikeUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardLikeUseCase.swift; sourceTree = ""; }; + 389E59B92EDEEB8000D0946D /* BlockUserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserUseCase.swift; sourceTree = ""; }; + 389E59BC2EDEEBEA00D0946D /* ReportCardUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCardUseCase.swift; sourceTree = ""; }; + 389E59BF2EDEEC4500D0946D /* DeleteCardUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteCardUseCase.swift; sourceTree = ""; }; + 389E59C22EDEEC7600D0946D /* CardImageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageUseCase.swift; sourceTree = ""; }; + 389E59C52EDEECBC00D0946D /* WriteCardUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardUseCase.swift; sourceTree = ""; }; + 389E59C82EDEED2700D0946D /* FetchTagUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTagUseCase.swift; sourceTree = ""; }; + 389E59CB2EDEED6100D0946D /* FetchUserInfoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUserInfoUseCase.swift; sourceTree = ""; }; + 389E59CE2EDEEDD000D0946D /* FetchFollowUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFollowUseCase.swift; sourceTree = ""; }; + 389E59D12EDEEE3800D0946D /* UpdateFollowUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFollowUseCase.swift; sourceTree = ""; }; + 389E59D42EDEEE6500D0946D /* UpdateUserInfoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateUserInfoUseCase.swift; sourceTree = ""; }; + 389E59D72EDEEEB900D0946D /* UpdateNotifyUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateNotifyUseCase.swift; sourceTree = ""; }; + 389E59DA2EDEEF3000D0946D /* FetchBlockUserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchBlockUserUseCase.swift; sourceTree = ""; }; + 389E59DD2EDEEF7600D0946D /* TransferAccountUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferAccountUseCase.swift; sourceTree = ""; }; + 389E59E02EDEEF9E00D0946D /* UpdateTagFavoriteUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTagFavoriteUseCase.swift; sourceTree = ""; }; + 389E59E32EDEF02900D0946D /* LocationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationUseCase.swift; sourceTree = ""; }; 389EF8162D2F450000E053AE /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 389EF8192D2F454600E053AE /* Log+Extract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Log+Extract.swift"; sourceTree = ""; }; 389EF81D2D2F469B00E053AE /* CocoaLumberjack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaLumberjack.swift; sourceTree = ""; }; @@ -1030,6 +1124,28 @@ 38AE77D62E7459EA00B6FD13 /* OnboardingCompletedViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCompletedViewReactor.swift; sourceTree = ""; }; 38AE77DA2E745FF700B6FD13 /* EnterMemberTransferTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterMemberTransferTextFieldView.swift; sourceTree = ""; }; 38AE77DD2E7465E600B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnterMemberTransferTextFieldView+Rx.swift"; sourceTree = ""; }; + 38AE85082EDF413C00029E4C /* BlockUserUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserUseCaseImpl.swift; sourceTree = ""; }; + 38AE850B2EDF41AF00029E4C /* CardImageUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageUseCaseImpl.swift; sourceTree = ""; }; + 38AE850E2EDF420300029E4C /* DeleteCardUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteCardUseCaseImpl.swift; sourceTree = ""; }; + 38AE85112EDF424600029E4C /* FetchBlockUserUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchBlockUserUseCaseImpl.swift; sourceTree = ""; }; + 38AE85142EDF42B400029E4C /* FetchCardDetailUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCardDetailUseCaseImpl.swift; sourceTree = ""; }; + 38AE85172EDF436F00029E4C /* FetchCardUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCardUseCaseImpl.swift; sourceTree = ""; }; + 38AE851A2EDFF7DE00029E4C /* FetchFollowUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFollowUseCaseImpl.swift; sourceTree = ""; }; + 38AE851D2EDFF84500029E4C /* FetchNoticeUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchNoticeUseCaseImpl.swift; sourceTree = ""; }; + 38AE85202EDFF88A00029E4C /* FetchTagUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTagUseCaseImpl.swift; sourceTree = ""; }; + 38AE85232EDFF90000029E4C /* FetchUserInfoUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUserInfoUseCaseImpl.swift; sourceTree = ""; }; + 38AE85262EDFF95200029E4C /* LocationUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationUseCaseImpl.swift; sourceTree = ""; }; + 38AE85292EDFF99B00029E4C /* ReportCardUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCardUseCaseImpl.swift; sourceTree = ""; }; + 38AE852C2EDFFA3900029E4C /* TransferAccountUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferAccountUseCaseImpl.swift; sourceTree = ""; }; + 38AE852F2EDFFA9300029E4C /* UpdateCardLikeUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardLikeUseCaseImpl.swift; sourceTree = ""; }; + 38AE85322EDFFAC300029E4C /* UpdateFollowUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFollowUseCaseImpl.swift; sourceTree = ""; }; + 38AE85352EDFFAF700029E4C /* UpdateNotifyUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateNotifyUseCaseImpl.swift; sourceTree = ""; }; + 38AE85382EDFFBBE00029E4C /* UpdateTagFavoriteUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTagFavoriteUseCaseImpl.swift; sourceTree = ""; }; + 38AE853B2EDFFBF500029E4C /* UpdateUserInfoUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateUserInfoUseCaseImpl.swift; sourceTree = ""; }; + 38AE853E2EDFFC3500029E4C /* UploadUserImageUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadUserImageUseCaseImpl.swift; sourceTree = ""; }; + 38AE85412EDFFCA400029E4C /* ValidateNicknameUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateNicknameUseCaseImpl.swift; sourceTree = ""; }; + 38AE85442EDFFCF800029E4C /* ValidateUserUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateUserUseCaseImpl.swift; sourceTree = ""; }; + 38AE85472EDFFD7B00029E4C /* WriteCardUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardUseCaseImpl.swift; sourceTree = ""; }; 38B21C012ECEF45D00990F49 /* FavoriteTagViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagViewModel.swift; sourceTree = ""; }; 38B21C072ECEF7CF00990F49 /* PopularTagViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularTagViewCell.swift; sourceTree = ""; }; 38B21C0A2ECEFF9F00990F49 /* FavoriteTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagsView.swift; sourceTree = ""; }; @@ -1062,8 +1178,6 @@ 38C2A7E62EC0718900B941A2 /* SettingsRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRemoteDataSourceImpl.swift; sourceTree = ""; }; 38C2A7E92EC0749A00B941A2 /* SettingsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRepository.swift; sourceTree = ""; }; 38C2A7EC2EC074AE00B941A2 /* SettingsRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRepositoryImpl.swift; sourceTree = ""; }; - 38C2A7EF2EC0795F00B941A2 /* SettingsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUseCase.swift; sourceTree = ""; }; - 38C2A7F22EC0798600B941A2 /* SettingsUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUseCaseImpl.swift; sourceTree = ""; }; 38C2A7F52EC08FEF00B941A2 /* BlockUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserInfo.swift; sourceTree = ""; }; 38C2A7F82EC090AC00B941A2 /* BlockUsersInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUsersInfoResponse.swift; sourceTree = ""; }; 38C2A7FB2EC0925700B941A2 /* WithdrawType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawType.swift; sourceTree = ""; }; @@ -1083,8 +1197,6 @@ 38C9AF192E96696500B401C0 /* TagRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagRemoteDataSourceImpl.swift; sourceTree = ""; }; 38C9AF1F2E9669F100B401C0 /* TagRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagRepository.swift; sourceTree = ""; }; 38C9AF222E966A1300B401C0 /* TagRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagRepositoryImpl.swift; sourceTree = ""; }; - 38C9AF252E966A6100B401C0 /* TagUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagUseCase.swift; sourceTree = ""; }; - 38C9AF282E966A8B00B401C0 /* TagUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagUseCaseImpl.swift; sourceTree = ""; }; 38C9AF2C2E96A3D600B401C0 /* WriteCardTagModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardTagModel.swift; sourceTree = ""; }; 38C9AF2F2E96A49B00B401C0 /* WriteCardTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardTag.swift; sourceTree = ""; }; 38C9AF322E96A82600B401C0 /* WriteCardTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardTags.swift; sourceTree = ""; }; @@ -1144,6 +1256,8 @@ 38F161422ECDA853003BADB6 /* SearchViewButton+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewButton+Rx.swift"; sourceTree = ""; }; 38F161462ECDA8D1003BADB6 /* FavoriteTagPlaceholderViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagPlaceholderViewCell.swift; sourceTree = ""; }; 38F161492ECDAD29003BADB6 /* FavoriteTagViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagViewCell.swift; sourceTree = ""; }; + 38F3398E2EE31C7E0066A5F7 /* IsCardDeletedResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsCardDeletedResponse.swift; sourceTree = ""; }; + 38F339912EE328710066A5F7 /* WriteCardGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardGuideView.swift; sourceTree = ""; }; 38F376092ECB772600E4A41D /* FavoriteTagInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagInfo.swift; sourceTree = ""; }; 38F3760C2ECB778C00E4A41D /* FavoriteTagInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteTagInfoResponse.swift; sourceTree = ""; }; 38F376102ECB789200E4A41D /* TagCardInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCardInfoResponse.swift; sourceTree = ""; }; @@ -2044,6 +2158,7 @@ 38899E632E7938CD0030F7CA /* Responses */ = { isa = PBXGroup; children = ( + 38F3398E2EE31C7E0066A5F7 /* IsCardDeletedResponse.swift */, 38F376102ECB789200E4A41D /* TagCardInfoResponse.swift */, 38F3760C2ECB778C00E4A41D /* FavoriteTagInfoResponse.swift */, 3879B4B72EC5ADBF0070846B /* RejoinableDateInfoResponse.swift */, @@ -2175,11 +2290,29 @@ children = ( 3889A24C2E79AEAD0030F7CA /* AppVersionUseCaseImpl.swift */, 3889A2882E79D81E0030F7CA /* AuthUseCaseImpl.swift */, - 380F42382E885057009AC59E /* CardUseCaseImpl.swift */, + 38AE85082EDF413C00029E4C /* BlockUserUseCaseImpl.swift */, + 38AE850B2EDF41AF00029E4C /* CardImageUseCaseImpl.swift */, + 38AE850E2EDF420300029E4C /* DeleteCardUseCaseImpl.swift */, + 38AE85112EDF424600029E4C /* FetchBlockUserUseCaseImpl.swift */, + 38AE85142EDF42B400029E4C /* FetchCardDetailUseCaseImpl.swift */, + 38AE85172EDF436F00029E4C /* FetchCardUseCaseImpl.swift */, + 38AE851A2EDFF7DE00029E4C /* FetchFollowUseCaseImpl.swift */, + 38AE851D2EDFF84500029E4C /* FetchNoticeUseCaseImpl.swift */, + 38AE85202EDFF88A00029E4C /* FetchTagUseCaseImpl.swift */, + 38AE85232EDFF90000029E4C /* FetchUserInfoUseCaseImpl.swift */, + 38AE85262EDFF95200029E4C /* LocationUseCaseImpl.swift */, 3889A2942E79D9200030F7CA /* NotificationUseCaseImpl.swift */, - 38C9AF282E966A8B00B401C0 /* TagUseCaseImpl.swift */, - 38C2A7F22EC0798600B941A2 /* SettingsUseCaseImpl.swift */, - 3889A2672E79BC840030F7CA /* UserUseCaseImpl.swift */, + 38AE85292EDFF99B00029E4C /* ReportCardUseCaseImpl.swift */, + 38AE852C2EDFFA3900029E4C /* TransferAccountUseCaseImpl.swift */, + 38AE852F2EDFFA9300029E4C /* UpdateCardLikeUseCaseImpl.swift */, + 38AE85322EDFFAC300029E4C /* UpdateFollowUseCaseImpl.swift */, + 38AE85352EDFFAF700029E4C /* UpdateNotifyUseCaseImpl.swift */, + 38AE85382EDFFBBE00029E4C /* UpdateTagFavoriteUseCaseImpl.swift */, + 38AE853B2EDFFBF500029E4C /* UpdateUserInfoUseCaseImpl.swift */, + 38AE853E2EDFFC3500029E4C /* UploadUserImageUseCaseImpl.swift */, + 38AE85412EDFFCA400029E4C /* ValidateNicknameUseCaseImpl.swift */, + 38AE85442EDFFCF800029E4C /* ValidateUserUseCaseImpl.swift */, + 38AE85472EDFFD7B00029E4C /* WriteCardUseCaseImpl.swift */, 3889A2482E79AE870030F7CA /* Interfaces */, ); path = UseCases; @@ -2204,11 +2337,29 @@ children = ( 3889A2492E79AE900030F7CA /* AppVersionUseCase.swift */, 3889A2852E79D8060030F7CA /* AuthUseCase.swift */, - 380F42352E88502F009AC59E /* CardUseCase.swift */, + 389E59B92EDEEB8000D0946D /* BlockUserUseCase.swift */, + 389E59C22EDEEC7600D0946D /* CardImageUseCase.swift */, + 389E59BF2EDEEC4500D0946D /* DeleteCardUseCase.swift */, + 389E59DA2EDEEF3000D0946D /* FetchBlockUserUseCase.swift */, + 389E59B32EDEEA7600D0946D /* FetchCardDetailUseCase.swift */, + 389E59AD2EDEE8B500D0946D /* FetchCardUseCase.swift */, + 389E59CE2EDEEDD000D0946D /* FetchFollowUseCase.swift */, + 389E59B02EDEEA3300D0946D /* FetchNoticeUseCase.swift */, + 389E59C82EDEED2700D0946D /* FetchTagUseCase.swift */, + 389E59CB2EDEED6100D0946D /* FetchUserInfoUseCase.swift */, + 389E59E32EDEF02900D0946D /* LocationUseCase.swift */, 3889A2912E79D8F40030F7CA /* NotificationUseCase.swift */, - 38C9AF252E966A6100B401C0 /* TagUseCase.swift */, - 38C2A7EF2EC0795F00B941A2 /* SettingsUseCase.swift */, - 3889A2642E79BBC00030F7CA /* UserUseCase.swift */, + 389E59BC2EDEEBEA00D0946D /* ReportCardUseCase.swift */, + 389E59DD2EDEEF7600D0946D /* TransferAccountUseCase.swift */, + 389E59B62EDEEAEB00D0946D /* UpdateCardLikeUseCase.swift */, + 389E59D12EDEEE3800D0946D /* UpdateFollowUseCase.swift */, + 389E59D72EDEEEB900D0946D /* UpdateNotifyUseCase.swift */, + 389E59E02EDEEF9E00D0946D /* UpdateTagFavoriteUseCase.swift */, + 389E59D42EDEEE6500D0946D /* UpdateUserInfoUseCase.swift */, + 389E59AA2EDEE73500D0946D /* UploadUserImageUseCase.swift */, + 389E59A72EDEE6EF00D0946D /* ValidateNicknameUseCase.swift */, + 389E59A42EDEE38E00D0946D /* ValidateUserUseCase.swift */, + 389E59C52EDEECBC00D0946D /* WriteCardUseCase.swift */, ); path = Interfaces; sourceTree = ""; @@ -2509,6 +2660,7 @@ 38C9AF2B2E96A37E00B401C0 /* Tags */, 381A1D672CC38E8E005FDB8E /* TextView */, 3887D0352CC5335D00FB52E1 /* WriteCardView.swift */, + 38F339912EE328710066A5F7 /* WriteCardGuideView.swift */, ); path = Views; sourceTree = ""; @@ -2939,13 +3091,11 @@ 38B8A5892CAEA5F9000AFE83 /* DetailViewCell.swift in Sources */, 389EF81F2D2F469B00E053AE /* CocoaLumberjack.swift in Sources */, 3802BDB92D0AF2F7001256EA /* PushManager+Rx.swift in Sources */, - 38C2A7F32EC0798B00B941A2 /* SettingsUseCaseImpl.swift in Sources */, 3880EF802EA0DB0900D88608 /* WriteCardTagsDelegate.swift in Sources */, 385620F72CA19EA900E0AB5A /* Alamofire_constants.swift in Sources */, 3817016F2CD882C2005FC220 /* TimeoutInterceptor.swift in Sources */, 38AE565D2D048B4800CAA431 /* SOMDialogViewController+Show.swift in Sources */, 38E928CB2EB7402200B3F00B /* WrittenTag.swift in Sources */, - 3889A2682E79BC880030F7CA /* UserUseCaseImpl.swift in Sources */, 38C9AF3C2E96ACEB00B401C0 /* WriteCardTagFooterDelegate.swift in Sources */, 3878D0762CFFE01500F9522F /* SettingVersionCellView.swift in Sources */, 3878D08D2CFFF0BF00F9522F /* AnnouncementViewControler.swift in Sources */, @@ -2966,6 +3116,8 @@ 385C01B12E8E8DD8003C7894 /* SOMPageView.swift in Sources */, 3803CF6A2D0156BA00FD90DB /* SettingsViewReactor.swift in Sources */, 381B83E52EBC73FC00C84015 /* FollowInfoResponse.swift in Sources */, + 389E59CF2EDEEDD500D0946D /* FetchFollowUseCase.swift in Sources */, + 389E59D92EDEEEC500D0946D /* UpdateNotifyUseCase.swift in Sources */, 3878F4782CA3F08300AA46A2 /* UIView.swift in Sources */, 3816E23B2D3BF402004CC196 /* TermsOfServiceCellView+Rx.swift in Sources */, 38773E7D2CB3ACB2004815CD /* SOMRefreshControl.swift in Sources */, @@ -2974,11 +3126,13 @@ 3889A24D2E79AEB30030F7CA /* AppVersionUseCaseImpl.swift in Sources */, 38CC49862CDE3885007A0145 /* SOMTransitioningDelegate.swift in Sources */, 388DA0FC2C8F521300A9DD56 /* FontContainer.swift in Sources */, + 389E59C32EDEEC7B00D0946D /* CardImageUseCase.swift in Sources */, 384972A12CA4DEC00012FCA1 /* CardRequest.swift in Sources */, 381B83E02EBC72B400C84015 /* FollowInfo.swift in Sources */, 38D488CB2D0C557300F2D38D /* SOMButton.swift in Sources */, 38C2A7F62EC08FF600B941A2 /* BlockUserInfo.swift in Sources */, 388FCAD12CFAC2BF0012C4D6 /* Notification.swift in Sources */, + 38F339922EE328750066A5F7 /* WriteCardGuideView.swift in Sources */, 3802BDB62D0AF16A001256EA /* PushManager.swift in Sources */, 38B6AADC2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift in Sources */, 38B543E02D46171300DDF2C5 /* ManagerConfiguration.swift in Sources */, @@ -2988,6 +3142,8 @@ 381854982E992E9900424D71 /* WriteCardSelectImageView.swift in Sources */, 385053592C92DD2300C80B02 /* SOMTabBarController.swift in Sources */, 386E966E2E9A53D6005E047D /* SelectOptionsView.swift in Sources */, + 389E59DF2EDEEF7C00D0946D /* TransferAccountUseCase.swift in Sources */, + 38AE85332EDFFAC400029E4C /* UpdateFollowUseCaseImpl.swift in Sources */, 383EC61D2E7A548E00EC2D1E /* BaseDIContainer.swift in Sources */, 385009C32D363525007175A1 /* FilterNil.swift in Sources */, 38FDC2B72C9E746B00C094C2 /* BaseViewController.swift in Sources */, @@ -2995,11 +3151,13 @@ 38E7FBF02D3CF6BC00A359CD /* SOMDialogAction.swift in Sources */, 3878D07E2CFFE6E500F9522F /* IssueMemberTransferViewController.swift in Sources */, 3878B8632D0DC8BD00B3B128 /* UIViewController+Toast.swift in Sources */, + 389E59AF2EDEE8BD00D0946D /* FetchCardUseCase.swift in Sources */, 38C2A7D82EC054C500B941A2 /* SettingVersionCellView+Rx.swift in Sources */, 38F1614B2ECDAD34003BADB6 /* FavoriteTagViewCell.swift in Sources */, 38E9CE112D376E0E00E85A2D /* PushTokenSet.swift in Sources */, 386867A42E9E378200171A5E /* Array.swift in Sources */, 38899E6F2E79400C0030F7CA /* ImageUrlInfoResponse.swift in Sources */, + 38AE852B2EDFF9CC00029E4C /* ReportCardUseCaseImpl.swift in Sources */, 3889A2772E79C29F0030F7CA /* NotificationRemoteDataSoruceImpl.swift in Sources */, 38F3760D2ECB779E00E4A41D /* FavoriteTagInfoResponse.swift in Sources */, 38C2A7EE2EC074B200B941A2 /* SettingsRepositoryImpl.swift in Sources */, @@ -3007,7 +3165,6 @@ 38FEBE552E865121002916A8 /* FollowNotificationInfoResponse.swift in Sources */, 38EBA90F2EB39920008B28F4 /* PostingPermission.swift in Sources */, 38899E9C2E7954D90030F7CA /* DeletedNotificationInfoResponse.swift in Sources */, - 38C2A7F02EC0796700B941A2 /* SettingsUseCase.swift in Sources */, 38899E8C2E794E690030F7CA /* AppVersionStatusResponse.swift in Sources */, 38C9AF2D2E96A3E500B401C0 /* WriteCardTagModel.swift in Sources */, 38A721962E73E7140071E1D8 /* View+SwiftEntryKit.swift in Sources */, @@ -3019,7 +3176,7 @@ 38B65E7C2E72ADB900DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift in Sources */, 38D2FBCE2E81B52F006DD739 /* SOMSwipableTabBar.swift in Sources */, 38FD4DB52D034F6600BF5FF1 /* FollowerViewCell.swift in Sources */, - 380F42372E885032009AC59E /* CardUseCase.swift in Sources */, + 389E59A82EDEE6F600D0946D /* ValidateNicknameUseCase.swift in Sources */, 382E153B2D15A67A0097B09C /* NotificationViewCell.swift in Sources */, 38C2A80C2EC0BC4500B941A2 /* BlockUsersViewController.swift in Sources */, 388009922CABF855002A9209 /* SOMTagModel.swift in Sources */, @@ -3029,6 +3186,7 @@ 383EC6122E7A4F6B00EC2D1E /* AuthLocalDataSource.swift in Sources */, 38C9AF3A2E96AB9100B401C0 /* WriteCardTagFooter.swift in Sources */, 385C01B82E8EA1EF003C7894 /* SOMPageViews.swift in Sources */, + 389E59D32EDEEE4100D0946D /* UpdateFollowUseCase.swift in Sources */, 385C01AE2E8E8C6F003C7894 /* SOMPageModel.swift in Sources */, 2A032EFE2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift in Sources */, 38787B822ED1EB21004BBAA7 /* TagCollectCardsView.swift in Sources */, @@ -3037,6 +3195,7 @@ 3878D0892CFFEF0F00F9522F /* TermsOfServiceTextCellView+Rx.swift in Sources */, 3878D0702CFFDF9600F9522F /* SettingTextCellView.swift in Sources */, 2A5BB7BF2CDB870000E1C799 /* OnboardingGuideMessageView.swift in Sources */, + 389E59CC2EDEED6500D0946D /* FetchUserInfoUseCase.swift in Sources */, 38FEBE612E8661F4002916A8 /* NoticeInfo.swift in Sources */, 3878D07A2CFFE1E800F9522F /* ResignViewController.swift in Sources */, 38899EAD2E79A09B0030F7CA /* UserRequest.swift in Sources */, @@ -3051,6 +3210,7 @@ 3800575D2D9C12CB00E58A19 /* DefinedError.swift in Sources */, 387FA11E2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */, 38899E942E79518F0030F7CA /* CommonNotificationInfo.swift in Sources */, + 389E59C92EDEED2B00D0946D /* FetchTagUseCase.swift in Sources */, 2AFD056E2D048CAF007C84AD /* TagRequest.swift in Sources */, 38CA91F42EBDCFF2002C261A /* ProfileCardsPlaceholderViewCell.swift in Sources */, 38E928D02EB75FA300B3F00B /* PungView.swift in Sources */, @@ -3061,6 +3221,8 @@ 38899E862E794CEE0030F7CA /* NetworkManager_FCM.swift in Sources */, 38EBA9112EB399A1008B28F4 /* PostingPermissionResponse.swift in Sources */, 38C2D4212CFEB82400CEA092 /* ProfileViewReactor.swift in Sources */, + 38AE85392EDFFBBF00029E4C /* UpdateTagFavoriteUseCaseImpl.swift in Sources */, + 38AE85452EDFFCF900029E4C /* ValidateUserUseCaseImpl.swift in Sources */, 38816D9F2D004A5E00EB87D6 /* UpdateProfileViewController.swift in Sources */, 38D6F1842CC243DB00E11530 /* UITextView+Typography.swift in Sources */, 38572CDF2D2254E800B07C69 /* HomePlaceholderViewCell.swift in Sources */, @@ -3075,6 +3237,7 @@ 38787B882ED22324004BBAA7 /* RxSwift+Unretained.swift in Sources */, 38B543E32D46179500DDF2C5 /* AuthManagerConfiguration.swift in Sources */, 38D055C42CD862FE00E75590 /* SOMActivityIndicatorView.swift in Sources */, + 38AE85182EDF437400029E4C /* FetchCardUseCaseImpl.swift in Sources */, 3880EF7B2EA0D17E00D88608 /* RelatedTagsView+Rx.swift in Sources */, 38B543EF2D46506300DDF2C5 /* ManagerType.swift in Sources */, 3850719B2CA295A800A7905A /* LaunchScreenViewController.swift in Sources */, @@ -3091,18 +3254,23 @@ 38899E892E794D620030F7CA /* NetworkManager_Version.swift in Sources */, 3803B92B2ECF5580009D14B9 /* TagCollectViewController.swift in Sources */, 385620F32CA19D2D00E0AB5A /* Alamofire_Request.swift in Sources */, + 389E59C72EDEECBF00D0946D /* WriteCardUseCase.swift in Sources */, 388A2D312D00D6A100E2F2F0 /* FollowViewReactor.swift in Sources */, 2A649ED42CAE990B002D8284 /* SOMDialogViewController.swift in Sources */, 3893B6CF2D36728000F2004C /* ManagerProvider.swift in Sources */, + 38AE85372EDFFAF900029E4C /* UpdateNotifyUseCaseImpl.swift in Sources */, 38C2A7FC2EC0925C00B941A2 /* WithdrawType.swift in Sources */, 38C2A7DC2EC06ECE00B941A2 /* SettingsRequest.swift in Sources */, 38A7219A2E73EA6F0071E1D8 /* SOMBottomFloatView.swift in Sources */, 3866577F2CEF3554009F7F60 /* UIButton+Rx.swift in Sources */, 385441912C870544004E2BB0 /* AppDelegate.swift in Sources */, + 389E59B52EDEEA8200D0946D /* FetchCardDetailUseCase.swift in Sources */, + 389E59E42EDEF03000D0946D /* LocationUseCase.swift in Sources */, 38D522692E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */, 38D869642CF821F900BF87DA /* UserDefaults.swift in Sources */, 380F422D2E884F3D009AC59E /* CardRemoteDataSourceImpl.swift in Sources */, 38C9AF332E96A82900B401C0 /* WriteCardTags.swift in Sources */, + 38F3398F2EE31C870066A5F7 /* IsCardDeletedResponse.swift in Sources */, 38899E672E7939600030F7CA /* CheckAvailableResponse.swift in Sources */, 38AE77DB2E745FFF00B6FD13 /* EnterMemberTransferTextFieldView.swift in Sources */, 3889A2432E79AD7D0030F7CA /* AppVersionRepository.swift in Sources */, @@ -3117,7 +3285,7 @@ 3889A24B2E79AE960030F7CA /* AppVersionUseCase.swift in Sources */, 2A980BA12D803EB1007DFA45 /* AnalyticsEventProtocol.swift in Sources */, 38F161472ECDA8F0003BADB6 /* FavoriteTagPlaceholderViewCell.swift in Sources */, - 38C9AF2A2E966A8F00B401C0 /* TagUseCaseImpl.swift in Sources */, + 389E59BB2EDEEB8300D0946D /* BlockUserUseCase.swift in Sources */, 2A5BB7E12CDCBE7E00E1C799 /* OnboardingNicknameSettingViewReactor.swift in Sources */, 38D8FE8D2EBE36F800F32D02 /* ProfileCardsViewCell.swift in Sources */, 38C2A7DF2EC0704700B941A2 /* SettingsRemoteDataSource.swift in Sources */, @@ -3137,10 +3305,12 @@ 3889A25C2E79BB340030F7CA /* UserRepository.swift in Sources */, 38899E7E2E794B420030F7CA /* SignUpResponse.swift in Sources */, 38CA91F72EBDD342002C261A /* ProfileViewHeader.swift in Sources */, + 38AE850C2EDF41B700029E4C /* CardImageUseCaseImpl.swift in Sources */, 381E7C232ECCC63E00E80249 /* SearchViewButton.swift in Sources */, 38B21C022ECEF46200990F49 /* FavoriteTagViewModel.swift in Sources */, 38AA00032CAD1BCC002C5F1E /* LikeAndCommentView.swift in Sources */, 38D2FBD12E81B9B7006DD739 /* SOMSwipableTabBarDelegate.swift in Sources */, + 389E59B12EDEEA3A00D0946D /* FetchNoticeUseCase.swift in Sources */, 38D2FBC12E812354006DD739 /* HomeViewController.swift in Sources */, 3816C0612CCDE35300C8688C /* ErrorInterceptor.swift in Sources */, 3803CF7B2D016BDB00FD90DB /* IssueMemberTransferViewReactor.swift in Sources */, @@ -3152,6 +3322,7 @@ 3889A2802E79D0250030F7CA /* Token.swift in Sources */, 38787B7E2ED1E8F4004BBAA7 /* TagSearchViewReactor.swift in Sources */, 38F161432ECDA858003BADB6 /* SearchViewButton+Rx.swift in Sources */, + 38AE850F2EDF420700029E4C /* DeleteCardUseCaseImpl.swift in Sources */, 3879B4B52EC5AD5E0070846B /* RejoinableDateInfo.swift in Sources */, 388DA0FF2C8F526C00A9DD56 /* UIFont.swift in Sources */, 38C9AF202E9669F600B401C0 /* TagRepository.swift in Sources */, @@ -3171,8 +3342,11 @@ 3889A2892E79D8220030F7CA /* AuthUseCaseImpl.swift in Sources */, 3880098F2CABF4C2002A9209 /* SOMTag.swift in Sources */, 3803CF892D01914200FD90DB /* ResignViewReactor.swift in Sources */, + 389E59BD2EDEEBF300D0946D /* ReportCardUseCase.swift in Sources */, 388A2D342D00D7BF00E2F2F0 /* UpdateProfileViewReactor.swift in Sources */, 38B543E62D4617CB00DDF2C5 /* PushManagerConfiguration.swift in Sources */, + 389E59B72EDEEAFC00D0946D /* UpdateCardLikeUseCase.swift in Sources */, + 389E59A52EDEE39500D0946D /* ValidateUserUseCase.swift in Sources */, 3889A2462E79ADCE0030F7CA /* AppVersionRepositoryImpl.swift in Sources */, 2AE6B14A2CBC15BF00FA5C3C /* ReportViewController.swift in Sources */, 388D8AE02E73E6190044BA79 /* SwiftEntryKit.swift in Sources */, @@ -3189,13 +3363,15 @@ 2A5BB7D62CDCA5C900E1C799 /* TermsOfServiceAgreeButtonView.swift in Sources */, 38E9CE142D37711600E85A2D /* OnboardingViewReactor.swift in Sources */, 38E928B92EB715C900B3F00B /* ReortType.swift in Sources */, + 38AE853D2EDFFBF700029E4C /* UpdateUserInfoUseCaseImpl.swift in Sources */, 38F3D9312D06C2370049F575 /* SOMAnimationTransitioning.swift in Sources */, 38B8A58F2CAEB61A000AFE83 /* DetailViewFooterCell.swift in Sources */, 383088092EDC7B8C00D99D88 /* SOMMessageBubbleView.swift in Sources */, 2A5BB7BA2CDB860D00E1C799 /* OnboardingTermsOfServiceViewController.swift in Sources */, 38787B7B2ED1E8B3004BBAA7 /* TagSearchViewController.swift in Sources */, 385441922C870544004E2BB0 /* SceneDelegate.swift in Sources */, - 38C9AF272E966A6300B401C0 /* TagUseCase.swift in Sources */, + 389E59C12EDEEC4900D0946D /* DeleteCardUseCase.swift in Sources */, + 38AE85162EDF42B700029E4C /* FetchCardDetailUseCaseImpl.swift in Sources */, 38787B782ED1E719004BBAA7 /* SearchTermsView.swift in Sources */, 38C9AF0F2E96602300B401C0 /* DefaultImagesResponse.swift in Sources */, 38D563852D1719B1006265AA /* SOMStickyTabBarItem.swift in Sources */, @@ -3207,19 +3383,21 @@ 3878D0822CFFEC6900F9522F /* TermsOfServiceViewController.swift in Sources */, 38B21C0E2ECF0F1D00990F49 /* PopularTagsView.swift in Sources */, 38C2A8122EC0BE0B00B941A2 /* BlockUsersViewReactor.swift in Sources */, - 3889A2662E79BBC40030F7CA /* UserUseCase.swift in Sources */, 38121E352CA6DA4000602499 /* Date.swift in Sources */, 38C2A7FA2EC090B100B941A2 /* BlockUsersInfoResponse.swift in Sources */, 38899E6B2E793AFD0030F7CA /* CheckAvailable.swift in Sources */, 38C9AF242E966A1B00B401C0 /* TagRepositoryImpl.swift in Sources */, 38FCF4192E9F88EA003AC3D8 /* WriteCardTags+Rx.swift in Sources */, 38C2D4182CFEAACA00CEA092 /* ProfileCardViewCell.swift in Sources */, + 389E59DC2EDEEF3600D0946D /* FetchBlockUserUseCase.swift in Sources */, 381DEA8C2CD4BBCB009F1FE9 /* WriteCardTextView+Rx.swift in Sources */, 3818549D2E992F7D00424D71 /* WriteCardDefaultImageCell.swift in Sources */, 3889A2752E79C1D80030F7CA /* NotificationRemoteDataSource.swift in Sources */, + 38AE85222EDFF88C00029E4C /* FetchTagUseCaseImpl.swift in Sources */, 386712C42E97734B00541389 /* UITextField.swift in Sources */, 38C9AF0B2E965EFB00B401C0 /* DefaultImages.swift in Sources */, 381A1D752CC3D799005FDB8E /* SOMTagsDelegate.swift in Sources */, + 38AE85092EDF414400029E4C /* BlockUserUseCaseImpl.swift in Sources */, 3874B5642ECB2613004CC22A /* SettingsLocalDataSourceImpl.swift in Sources */, 3803CF832D017DB800FD90DB /* EnterMemberTransferViewController.swift in Sources */, 388693A02CF77FA7005F9EF3 /* UIApplication+Top.swift in Sources */, @@ -3229,6 +3407,8 @@ 388372022C8C8FCF004212EB /* UIColor.swift in Sources */, 38B543E92D4617EA00DDF2C5 /* NetworkManagerConfiguration.swift in Sources */, 38C9AF112E96656600B401C0 /* TagInfo.swift in Sources */, + 38AE852E2EDFFA3C00029E4C /* TransferAccountUseCaseImpl.swift in Sources */, + 38AE85432EDFFCA600029E4C /* ValidateNicknameUseCaseImpl.swift in Sources */, 388698592D1982DE00008600 /* NotificationViewController.swift in Sources */, 38389B9D2CCCF98B006728AF /* AuthRequest.swift in Sources */, 385E65A42CBE56D00032E120 /* Coordinate.swift in Sources */, @@ -3244,6 +3424,7 @@ 38899EA62E799BD60030F7CA /* AppVersionRemoteDataSourceImpl.swift in Sources */, 38AE77D42E74580000B6FD13 /* OnboardingCompletedViewController.swift in Sources */, 389EF81B2D2F454600E053AE /* Log+Extract.swift in Sources */, + 38AE85482EDFFD7E00029E4C /* WriteCardUseCaseImpl.swift in Sources */, 381701792CD88854005FC220 /* LogginMonitor.swift in Sources */, 386E966B2E9A51D9005E047D /* SelectOptionItem.swift in Sources */, 388DA1052C8F545E00A9DD56 /* Typography+SOOUM.swift in Sources */, @@ -3251,27 +3432,35 @@ 3880EF752EA0CEEE00D88608 /* RelatedTagsView.swift in Sources */, 3887D0372CC5335D00FB52E1 /* WriteCardView.swift in Sources */, 381E7C1C2ECCB1AD00E80249 /* TagViewReactor.swift in Sources */, + 38AE85252EDFF90400029E4C /* FetchUserInfoUseCaseImpl.swift in Sources */, 38C9AF1B2E96696C00B401C0 /* TagRemoteDataSourceImpl.swift in Sources */, 3889A2952E79D9250030F7CA /* NotificationUseCaseImpl.swift in Sources */, 38B8A58C2CAEA79A000AFE83 /* DetailViewFooter.swift in Sources */, 3889A2512E79B3260030F7CA /* UserRemoteDataSource.swift in Sources */, 380F42222E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */, + 38AE85132EDF424800029E4C /* FetchBlockUserUseCaseImpl.swift in Sources */, 38899E5E2E7937E50030F7CA /* NicknameValidateResponse.swift in Sources */, + 38AE851C2EDFF7E000029E4C /* FetchFollowUseCaseImpl.swift in Sources */, 385053532C92DBE200C80B02 /* SOMTabBarItem.swift in Sources */, 38C2A8092EC0BB9800B941A2 /* BlockUserViewCell.swift in Sources */, 3880EF6F2EA0CD7100D88608 /* RelatedTagViewModel.swift in Sources */, 3889A28F2E79D8860030F7CA /* NotificationRepositoryImpl.swift in Sources */, 38B65E7A2E72A29F00DF6919 /* OnboardingNumberingView.swift in Sources */, 3889A28C2E79D86B0030F7CA /* NotificationRepository.swift in Sources */, + 38AE853F2EDFFC3600029E4C /* UploadUserImageUseCaseImpl.swift in Sources */, 38B543EC2D461B1A00DDF2C5 /* LocationManagerConfigruation.swift in Sources */, 38A5D1552C8CB12300B68363 /* UIImage+SOOUM.swift in Sources */, 385602B72D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */, + 389E59E12EDEEFA500D0946D /* UpdateTagFavoriteUseCase.swift in Sources */, 383EC6152E7A50EB00EC2D1E /* AuthLocalDataSourceImpl.swift in Sources */, 38D8F5592EC4D89D00DED428 /* TagNofificationInfoResponse.swift in Sources */, 38C9AF302E96A49F00B401C0 /* WriteCardTag.swift in Sources */, + 38AE85312EDFFA9500029E4C /* UpdateCardLikeUseCaseImpl.swift in Sources */, + 389E59D52EDEEE6B00D0946D /* UpdateUserInfoUseCase.swift in Sources */, 3889A27D2E79C56E0030F7CA /* ToeknResponse.swift in Sources */, 3878D06C2CFFDF1F00F9522F /* SettingsViewController.swift in Sources */, 3803B9262ECF530C009D14B9 /* TagCollectPlaceholderViewCell.swift in Sources */, + 38AE851E2EDFF84700029E4C /* FetchNoticeUseCaseImpl.swift in Sources */, 38FD4DAF2D032FCE00BF5FF1 /* AnnouncementViewReactor.swift in Sources */, 3802BDAD2D0AC1FB001256EA /* UIImage.swift in Sources */, 3889A2622E79BB5B0030F7CA /* UserRepositoryImpl.swift in Sources */, @@ -3286,10 +3475,11 @@ 3878F4722CA3F03400AA46A2 /* SOMCard.swift in Sources */, 3803CF862D017DC700FD90DB /* EnterMemberTransferViewReactor.swift in Sources */, 38B6AAE32CA4787200CE6DB6 /* MainTabBarReactor.swift in Sources */, - 380F42392E88505B009AC59E /* CardUseCaseImpl.swift in Sources */, 38899E592E7936DD0030F7CA /* SooumStyle_V2.swift in Sources */, + 38AE85272EDFF95500029E4C /* LocationUseCaseImpl.swift in Sources */, 380F42342E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */, 38FCF41C2EA00625003AC3D8 /* UITtextView.swift in Sources */, + 389E59AC2EDEE74B00D0946D /* UploadUserImageUseCase.swift in Sources */, 38B8A5852CAE9CC4000AFE83 /* HomeViewCell.swift in Sources */, 3878D0642CFFD66700F9522F /* FollowViewController.swift in Sources */, 38026E402CA2B45A0045E1CE /* LocationManager.swift in Sources */, @@ -3325,13 +3515,11 @@ 38F88EBA2D2C1CB8002AD7A8 /* Info.swift in Sources */, 38D563842D1719B1006265AA /* SOMStickyTabBarItem.swift in Sources */, 2A5BB7BE2CDB870000E1C799 /* OnboardingGuideMessageView.swift in Sources */, - 38C2A7F42EC0798B00B941A2 /* SettingsUseCaseImpl.swift in Sources */, 3880EF812EA0DB0900D88608 /* WriteCardTagsDelegate.swift in Sources */, 3836ACB42C8F045300A3C566 /* Typography.swift in Sources */, 38AE565C2D048B4800CAA431 /* SOMDialogViewController+Show.swift in Sources */, 38E928CA2EB7402200B3F00B /* WrittenTag.swift in Sources */, 385E65A32CBE56D00032E120 /* Coordinate.swift in Sources */, - 3889A2692E79BC880030F7CA /* UserUseCaseImpl.swift in Sources */, 38C9AF3D2E96ACEB00B401C0 /* WriteCardTagFooterDelegate.swift in Sources */, 3878FE0D2D0365C800D8955C /* SOMNavigationBar+Rx.swift in Sources */, 38FD4DB42D034F6600BF5FF1 /* FollowerViewCell.swift in Sources */, @@ -3351,6 +3539,8 @@ 385C01B22E8E8DD8003C7894 /* SOMPageView.swift in Sources */, 3830FFA62CEC6E3100ABA9FD /* Kingfisher.swift in Sources */, 381B83E62EBC73FC00C84015 /* FollowInfoResponse.swift in Sources */, + 389E59D02EDEEDD500D0946D /* FetchFollowUseCase.swift in Sources */, + 389E59D82EDEEEC500D0946D /* UpdateNotifyUseCase.swift in Sources */, 381701782CD88854005FC220 /* LogginMonitor.swift in Sources */, 3816E23A2D3BF402004CC196 /* TermsOfServiceCellView+Rx.swift in Sources */, 2A649ECF2CAE8970002D8284 /* SOMDialogViewController.swift in Sources */, @@ -3359,11 +3549,13 @@ 388009912CABF855002A9209 /* SOMTagModel.swift in Sources */, 3889A2562E79BA160030F7CA /* UserRemoteDataSourceImpl.swift in Sources */, 385C01B52E8EA1B7003C7894 /* SOMPageViewsDelegate.swift in Sources */, + 389E59C42EDEEC7B00D0946D /* CardImageUseCase.swift in Sources */, 3889A24E2E79AEB30030F7CA /* AppVersionUseCaseImpl.swift in Sources */, 388FCAD02CFAC2BF0012C4D6 /* Notification.swift in Sources */, 381B83DF2EBC72B400C84015 /* FollowInfo.swift in Sources */, 388C96362CCE41700061C598 /* AuthInfo.swift in Sources */, 38C2A7F72EC08FF600B941A2 /* BlockUserInfo.swift in Sources */, + 38F339932EE328750066A5F7 /* WriteCardGuideView.swift in Sources */, 388DA0FB2C8F521000A9DD56 /* FontContainer.swift in Sources */, 38B543DF2D46171300DDF2C5 /* ManagerConfiguration.swift in Sources */, 38D5637E2D17152F006265AA /* SOMStickyTabBarDelegate.swift in Sources */, @@ -3373,6 +3565,8 @@ 386E966F2E9A53D6005E047D /* SelectOptionsView.swift in Sources */, 38B6AADB2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift in Sources */, 383EC61C2E7A548E00EC2D1E /* BaseDIContainer.swift in Sources */, + 389E59DE2EDEEF7C00D0946D /* TransferAccountUseCase.swift in Sources */, + 38AE85342EDFFAC400029E4C /* UpdateFollowUseCaseImpl.swift in Sources */, 2A5BB7D12CDC7ADC00E1C799 /* OnboardingViewController.swift in Sources */, 3887D0392CC5504500FB52E1 /* UITextField+Typography.swift in Sources */, 3803CF7A2D016BDB00FD90DB /* IssueMemberTransferViewReactor.swift in Sources */, @@ -3380,11 +3574,13 @@ 38E7FBEF2D3CF6BB00A359CD /* SOMDialogAction.swift in Sources */, 38AA00022CAD1BCC002C5F1E /* LikeAndCommentView.swift in Sources */, 38C2A7D92EC054C500B941A2 /* SettingVersionCellView+Rx.swift in Sources */, + 389E59AE2EDEE8BD00D0946D /* FetchCardUseCase.swift in Sources */, 38D5CE0B2CBCE8CA0054AB9A /* SimpleDefaults.swift in Sources */, 38F1614A2ECDAD34003BADB6 /* FavoriteTagViewCell.swift in Sources */, 38E9CE102D376E0E00E85A2D /* PushTokenSet.swift in Sources */, 386867A52E9E378200171A5E /* Array.swift in Sources */, 38F3760E2ECB779E00E4A41D /* FavoriteTagInfoResponse.swift in Sources */, + 38AE852A2EDFF9CC00029E4C /* ReportCardUseCaseImpl.swift in Sources */, 385053582C92DD2300C80B02 /* SOMTabBarController.swift in Sources */, 38899E6E2E79400C0030F7CA /* ImageUrlInfoResponse.swift in Sources */, 3889A2782E79C29F0030F7CA /* NotificationRemoteDataSoruceImpl.swift in Sources */, @@ -3395,7 +3591,6 @@ 3878D0792CFFE1E800F9522F /* ResignViewController.swift in Sources */, 38EBA90E2EB39920008B28F4 /* PostingPermission.swift in Sources */, 38899E8D2E794E690030F7CA /* AppVersionStatusResponse.swift in Sources */, - 38C2A7F12EC0796700B941A2 /* SettingsUseCase.swift in Sources */, 38A721952E73E7140071E1D8 /* View+SwiftEntryKit.swift in Sources */, 38C9AF2E2E96A3E500B401C0 /* WriteCardTagModel.swift in Sources */, 380F422A2E884E9C009AC59E /* HomeCardInfoResponse.swift in Sources */, @@ -3404,8 +3599,8 @@ 2A980BA42D803EEA007DFA45 /* SOMEvent.swift in Sources */, 38B65E7D2E72ADB900DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift in Sources */, 38E928D92EB7727400B3F00B /* SOMBottomToastView.swift in Sources */, + 389E59A92EDEE6F600D0946D /* ValidateNicknameUseCase.swift in Sources */, 38D2FBCF2E81B52F006DD739 /* SOMSwipableTabBar.swift in Sources */, - 380F42362E885032009AC59E /* CardUseCase.swift in Sources */, 388009942CABFAAA002A9209 /* SOMTags.swift in Sources */, 38C2A80B2EC0BC4500B941A2 /* BlockUsersViewController.swift in Sources */, 3862C0DF2C9EB6670023C046 /* UIViewController+PushAndPop.swift in Sources */, @@ -3414,6 +3609,7 @@ 382D5CF62CFE9B8600BFA23E /* ProfileViewController.swift in Sources */, 38773E7C2CB3ACB2004815CD /* SOMRefreshControl.swift in Sources */, 3817016E2CD882C2005FC220 /* TimeoutInterceptor.swift in Sources */, + 389E59D22EDEEE4100D0946D /* UpdateFollowUseCase.swift in Sources */, 383EC6112E7A4F6B00EC2D1E /* AuthLocalDataSource.swift in Sources */, 388A2D302D00D6A100E2F2F0 /* FollowViewReactor.swift in Sources */, 38787B812ED1EB21004BBAA7 /* TagCollectCardsView.swift in Sources */, @@ -3422,6 +3618,7 @@ 385C01B72E8EA1EF003C7894 /* SOMPageViews.swift in Sources */, 385C01AF2E8E8C6F003C7894 /* SOMPageModel.swift in Sources */, 3880098E2CABF4C2002A9209 /* SOMTag.swift in Sources */, + 389E59CD2EDEED6500D0946D /* FetchUserInfoUseCase.swift in Sources */, 38C2A8012EC09A5A00B941A2 /* ResignTextFieldView.swift in Sources */, 38C2D4202CFEB82400CEA092 /* ProfileViewReactor.swift in Sources */, 38CC49822CDE3854007A0145 /* SOMPresentationController.swift in Sources */, @@ -3436,6 +3633,7 @@ 38D8E2912CCD232B00CE2E0A /* AuthManager.swift in Sources */, 3889A2862E79D8090030F7CA /* AuthUseCase.swift in Sources */, 3886985F2D1984D600008600 /* NotificationViewReactor.swift in Sources */, + 389E59CA2EDEED2B00D0946D /* FetchTagUseCase.swift in Sources */, 380F42312E884FBC009AC59E /* CardRepository.swift in Sources */, 387FA11D2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */, 3800575C2D9C12CB00E58A19 /* DefinedError.swift in Sources */, @@ -3446,6 +3644,8 @@ 3816C0602CCDE35300C8688C /* ErrorInterceptor.swift in Sources */, 38899E872E794CEE0030F7CA /* NetworkManager_FCM.swift in Sources */, 3879B4B92EC5ADC50070846B /* RejoinableDateInfoResponse.swift in Sources */, + 38AE853A2EDFFBBF00029E4C /* UpdateTagFavoriteUseCaseImpl.swift in Sources */, + 38AE85462EDFFCF900029E4C /* ValidateUserUseCaseImpl.swift in Sources */, 38CA91F32EBDCFF2002C261A /* ProfileCardsPlaceholderViewCell.swift in Sources */, 38E928D12EB75FA300B3F00B /* PungView.swift in Sources */, 38E928C72EB73FF800B3F00B /* WrittenTagModel.swift in Sources */, @@ -3460,6 +3660,7 @@ 38787B892ED22324004BBAA7 /* RxSwift+Unretained.swift in Sources */, 38B543E22D46179500DDF2C5 /* AuthManagerConfiguration.swift in Sources */, 38B8A58B2CAEA79A000AFE83 /* DetailViewFooter.swift in Sources */, + 38AE85192EDF437400029E4C /* FetchCardUseCaseImpl.swift in Sources */, 38B543EE2D46506300DDF2C5 /* ManagerType.swift in Sources */, 38C2A8052EC09BC400B941A2 /* ResignTextFieldView+Rx.swift in Sources */, 2A5BB7E02CDCBE7E00E1C799 /* OnboardingNicknameSettingViewReactor.swift in Sources */, @@ -3476,18 +3677,23 @@ 38899E8A2E794D620030F7CA /* NetworkManager_Version.swift in Sources */, 3803B92A2ECF5580009D14B9 /* TagCollectViewController.swift in Sources */, 388698582D1982DE00008600 /* NotificationViewController.swift in Sources */, + 389E59C62EDEECBF00D0946D /* WriteCardUseCase.swift in Sources */, 38C2A7E22EC0707D00B941A2 /* TransferCodeInfo.swift in Sources */, 38121E342CA6DA4000602499 /* Date.swift in Sources */, 3878D0752CFFE01500F9522F /* SettingVersionCellView.swift in Sources */, + 38AE85362EDFFAF900029E4C /* UpdateNotifyUseCaseImpl.swift in Sources */, 38C2A7EA2EC074A200B941A2 /* SettingsRepository.swift in Sources */, 38A721992E73EA6F0071E1D8 /* SOMBottomFloatView.swift in Sources */, 3893B6CE2D36728000F2004C /* ManagerProvider.swift in Sources */, 3880097B2CABEE3D002A9209 /* DetailViewController.swift in Sources */, 38D522682E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */, + 389E59B42EDEEA8200D0946D /* FetchCardDetailUseCase.swift in Sources */, + 389E59E52EDEF03000D0946D /* LocationUseCase.swift in Sources */, 38C2A7FD2EC0925C00B941A2 /* WithdrawType.swift in Sources */, 38C2A7DB2EC06ECE00B941A2 /* SettingsRequest.swift in Sources */, 38D869632CF821F900BF87DA /* UserDefaults.swift in Sources */, 380F422E2E884F3D009AC59E /* CardRemoteDataSourceImpl.swift in Sources */, + 38F339902EE31C870066A5F7 /* IsCardDeletedResponse.swift in Sources */, 38899E662E7939600030F7CA /* CheckAvailableResponse.swift in Sources */, 38C9AF342E96A82900B401C0 /* WriteCardTags.swift in Sources */, 385620F22CA19D2D00E0AB5A /* Alamofire_Request.swift in Sources */, @@ -3502,10 +3708,10 @@ 387FBAF02C8702C100A5E139 /* AppDelegate.swift in Sources */, 380F42272E884B80009AC59E /* BaseCardInfo.swift in Sources */, 38F161482ECDA8F0003BADB6 /* FavoriteTagPlaceholderViewCell.swift in Sources */, + 389E59BA2EDEEB8300D0946D /* BlockUserUseCase.swift in Sources */, 3889A24A2E79AE960030F7CA /* AppVersionUseCase.swift in Sources */, 2A980BA02D803EB1007DFA45 /* AnalyticsEventProtocol.swift in Sources */, 38B8A58E2CAEB61A000AFE83 /* DetailViewFooterCell.swift in Sources */, - 38C9AF292E966A8F00B401C0 /* TagUseCaseImpl.swift in Sources */, 2A5BB7C92CDBA53E00E1C799 /* OnboardingNicknameSettingViewController.swift in Sources */, 2AE6B1492CBC15BF00FA5C3C /* ReportViewController.swift in Sources */, 3887D0332CC5335200FB52E1 /* WriteCardViewReactor.swift in Sources */, @@ -3522,10 +3728,12 @@ 381B83DC2EBC707A00C84015 /* ProfileInfo.swift in Sources */, 38D8F55F2EC4F38700DED428 /* SimpleReachability.swift in Sources */, 38899E712E79402C0030F7CA /* ImageUrlInfo.swift in Sources */, + 38AE850D2EDF41B700029E4C /* CardImageUseCaseImpl.swift in Sources */, 381E7C242ECCC63E00E80249 /* SearchViewButton.swift in Sources */, 38B21C032ECEF46200990F49 /* FavoriteTagViewModel.swift in Sources */, 3880EF772EA0CF2F00D88608 /* RelatedTagsViewLayout.swift in Sources */, 381B83F22EBCEC2E00C84015 /* ProfileUserViewCell.swift in Sources */, + 389E59B22EDEEA3A00D0946D /* FetchNoticeUseCase.swift in Sources */, 3889A25D2E79BB340030F7CA /* UserRepository.swift in Sources */, 385620EF2CA19C9500E0AB5A /* NetworkManager.swift in Sources */, 38899E7D2E794B420030F7CA /* SignUpResponse.swift in Sources */, @@ -3537,6 +3745,7 @@ 381701712CD88374005FC220 /* CompositeInterceptor.swift in Sources */, 38787B7F2ED1E8F4004BBAA7 /* TagSearchViewReactor.swift in Sources */, 38F161442ECDA858003BADB6 /* SearchViewButton+Rx.swift in Sources */, + 38AE85102EDF420700029E4C /* DeleteCardUseCaseImpl.swift in Sources */, 38FDC2C72C9E764300C094C2 /* BaseNavigationViewController.swift in Sources */, 381B83E92EBC75D700C84015 /* ProfileCardInfo.swift in Sources */, 38D5637B2D16D72D006265AA /* SOMStickyTabBar.swift in Sources */, @@ -3556,8 +3765,11 @@ 38F3760B2ECB772A00E4A41D /* FavoriteTagInfo.swift in Sources */, 3889A28A2E79D8220030F7CA /* AuthUseCaseImpl.swift in Sources */, 38D8FE902EBE664C00F32D02 /* SOMNicknameTextField.swift in Sources */, + 389E59BE2EDEEBF300D0946D /* ReportCardUseCase.swift in Sources */, 388DA0FE2C8F526C00A9DD56 /* UIFont.swift in Sources */, 38B543E52D4617CB00DDF2C5 /* PushManagerConfiguration.swift in Sources */, + 389E59B82EDEEAFC00D0946D /* UpdateCardLikeUseCase.swift in Sources */, + 389E59A62EDEE39500D0946D /* ValidateUserUseCase.swift in Sources */, 3889A2472E79ADCE0030F7CA /* AppVersionRepositoryImpl.swift in Sources */, 388D8ADF2E73E6190044BA79 /* SwiftEntryKit.swift in Sources */, 38899E972E7953310030F7CA /* NotificationInfoResponse.swift in Sources */, @@ -3574,22 +3786,23 @@ 38E9CE132D37711600E85A2D /* OnboardingViewReactor.swift in Sources */, 3836ACBA2C8F050D00A3C566 /* UILabel+Typography.swift in Sources */, 38CC49852CDE3885007A0145 /* SOMTransitioningDelegate.swift in Sources */, + 38AE853C2EDFFBF700029E4C /* UpdateUserInfoUseCaseImpl.swift in Sources */, 388A2D332D00D7BF00E2F2F0 /* UpdateProfileViewReactor.swift in Sources */, 38E928BA2EB715C900B3F00B /* ReortType.swift in Sources */, 3830880A2EDC7B8C00D99D88 /* SOMMessageBubbleView.swift in Sources */, 3887D0362CC5335D00FB52E1 /* WriteCardView.swift in Sources */, 38787B7C2ED1E8B3004BBAA7 /* TagSearchViewController.swift in Sources */, 2A5BB7CD2CDBB7D100E1C799 /* OnboardingProfileImageSettingViewController.swift in Sources */, + 389E59C02EDEEC4900D0946D /* DeleteCardUseCase.swift in Sources */, + 38AE85152EDF42B700029E4C /* FetchCardDetailUseCaseImpl.swift in Sources */, 385053552C92DCF900C80B02 /* SOMTabBar.swift in Sources */, 38787B792ED1E719004BBAA7 /* SearchTermsView.swift in Sources */, 387FBAF22C8702C100A5E139 /* SceneDelegate.swift in Sources */, - 38C9AF262E966A6300B401C0 /* TagUseCase.swift in Sources */, 38121E312CA6C77500602499 /* Double.swift in Sources */, 38C9AF0E2E96602300B401C0 /* DefaultImagesResponse.swift in Sources */, 3889A26C2E79BD450030F7CA /* AuthRemoteDataSource.swift in Sources */, 38C2A7E82EC0719200B941A2 /* SettingsRemoteDataSourceImpl.swift in Sources */, 3889A2922E79D8F80030F7CA /* NotificationUseCase.swift in Sources */, - 3889A2652E79BBC40030F7CA /* UserUseCase.swift in Sources */, 38B21C0F2ECF0F1D00990F49 /* PopularTagsView.swift in Sources */, 384972A32CA54DC10012FCA1 /* UIImgeView.swift in Sources */, 38899E6C2E793AFD0030F7CA /* CheckAvailable.swift in Sources */, @@ -3599,12 +3812,15 @@ 38C2A7F92EC090B100B941A2 /* BlockUsersInfoResponse.swift in Sources */, 38C9AF232E966A1B00B401C0 /* TagRepositoryImpl.swift in Sources */, 38FCF41A2E9F88EA003AC3D8 /* WriteCardTags+Rx.swift in Sources */, + 389E59DB2EDEEF3600D0946D /* FetchBlockUserUseCase.swift in Sources */, 3889A2742E79C1D80030F7CA /* NotificationRemoteDataSource.swift in Sources */, 3818549C2E992F7D00424D71 /* WriteCardDefaultImageCell.swift in Sources */, 3878D07D2CFFE6E500F9522F /* IssueMemberTransferViewController.swift in Sources */, + 38AE85212EDFF88C00029E4C /* FetchTagUseCaseImpl.swift in Sources */, 386712C52E97734B00541389 /* UITextField.swift in Sources */, 38C9AF0C2E965EFB00B401C0 /* DefaultImages.swift in Sources */, 381DEA8B2CD4BBCB009F1FE9 /* WriteCardTextView+Rx.swift in Sources */, + 38AE850A2EDF414400029E4C /* BlockUserUseCaseImpl.swift in Sources */, 3878D08C2CFFF0BF00F9522F /* AnnouncementViewControler.swift in Sources */, 3874B5632ECB2613004CC22A /* SettingsLocalDataSourceImpl.swift in Sources */, 38B8BE472D1ECBDA0084569C /* PushNotificationInfo.swift in Sources */, @@ -3614,6 +3830,8 @@ 38C2A80F2EC0BC8900B941A2 /* BlockUserPlaceholderViewCell.swift in Sources */, 38C9AF122E96656600B401C0 /* TagInfo.swift in Sources */, 2AFD054C2CFF76CB007C84AD /* TagRequest.swift in Sources */, + 38AE852D2EDFFA3C00029E4C /* TransferAccountUseCaseImpl.swift in Sources */, + 38AE85422EDFFCA600029E4C /* ValidateNicknameUseCaseImpl.swift in Sources */, 38B6AADF2CA4777200CE6DB6 /* UIViewController+Rx.swift in Sources */, 389681102CAFBD6A00FFD89F /* DetailViewReactor.swift in Sources */, 38E928B72EB711E200B3F00B /* DetailCardInfo.swift in Sources */, @@ -3629,6 +3847,7 @@ 38899EA72E799BD60030F7CA /* AppVersionRemoteDataSourceImpl.swift in Sources */, 38AE77D52E74580000B6FD13 /* OnboardingCompletedViewController.swift in Sources */, 3878D06F2CFFDF9600F9522F /* SettingTextCellView.swift in Sources */, + 38AE85492EDFFD7E00029E4C /* WriteCardUseCaseImpl.swift in Sources */, 38B8A5842CAE9CC4000AFE83 /* HomeViewCell.swift in Sources */, 3803CF852D017DC700FD90DB /* EnterMemberTransferViewReactor.swift in Sources */, 386E966C2E9A51D9005E047D /* SelectOptionItem.swift in Sources */, @@ -3636,27 +3855,35 @@ 38D2FBCB2E81B0E5006DD739 /* SOMSwipableTabBarItem.swift in Sources */, 3880EF742EA0CEEE00D88608 /* RelatedTagsView.swift in Sources */, 381E7C1D2ECCB1AD00E80249 /* TagViewReactor.swift in Sources */, + 38AE85242EDFF90400029E4C /* FetchUserInfoUseCaseImpl.swift in Sources */, 3878F4712CA3F03400AA46A2 /* SOMCard.swift in Sources */, 3878D06B2CFFDF1F00F9522F /* SettingsViewController.swift in Sources */, 38C9AF1A2E96696C00B401C0 /* TagRemoteDataSourceImpl.swift in Sources */, 3889A2962E79D9250030F7CA /* NotificationUseCaseImpl.swift in Sources */, 3878D0882CFFEF0F00F9522F /* TermsOfServiceTextCellView+Rx.swift in Sources */, + 38AE85122EDF424800029E4C /* FetchBlockUserUseCaseImpl.swift in Sources */, 3889A2502E79B3260030F7CA /* UserRemoteDataSource.swift in Sources */, + 38AE851B2EDFF7E000029E4C /* FetchFollowUseCaseImpl.swift in Sources */, 380F42212E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */, 38899E5F2E7937E50030F7CA /* NicknameValidateResponse.swift in Sources */, 38C2A8082EC0BB9800B941A2 /* BlockUserViewCell.swift in Sources */, 3880EF6E2EA0CD7100D88608 /* RelatedTagViewModel.swift in Sources */, 3889A2902E79D8860030F7CA /* NotificationRepositoryImpl.swift in Sources */, 38B6AAE22CA4787200CE6DB6 /* MainTabBarReactor.swift in Sources */, + 38AE85402EDFFC3600029E4C /* UploadUserImageUseCaseImpl.swift in Sources */, 38B65E792E72A29F00DF6919 /* OnboardingNumberingView.swift in Sources */, 2A5BB7D52CDCA5C900E1C799 /* TermsOfServiceAgreeButtonView.swift in Sources */, 3889A28D2E79D86B0030F7CA /* NotificationRepository.swift in Sources */, + 389E59E22EDEEFA500D0946D /* UpdateTagFavoriteUseCase.swift in Sources */, 38B543EB2D461B1A00DDF2C5 /* LocationManagerConfigruation.swift in Sources */, 38026E3F2CA2B45A0045E1CE /* LocationManager.swift in Sources */, 383EC6162E7A50EB00EC2D1E /* AuthLocalDataSourceImpl.swift in Sources */, + 38AE85302EDFFA9500029E4C /* UpdateCardLikeUseCaseImpl.swift in Sources */, + 389E59D62EDEEE6B00D0946D /* UpdateUserInfoUseCase.swift in Sources */, 3878D0852CFFED7800F9522F /* TermsOfServiceTextCellView.swift in Sources */, 38D8F5582EC4D89D00DED428 /* TagNofificationInfoResponse.swift in Sources */, 3803B9272ECF530C009D14B9 /* TagCollectPlaceholderViewCell.swift in Sources */, + 38AE851F2EDFF84700029E4C /* FetchNoticeUseCaseImpl.swift in Sources */, 38C9AF312E96A49F00B401C0 /* WriteCardTag.swift in Sources */, 3889A27E2E79C56E0030F7CA /* ToeknResponse.swift in Sources */, 3878D0812CFFEC6900F9522F /* TermsOfServiceViewController.swift in Sources */, @@ -3673,8 +3900,9 @@ 38AE77DF2E7465F500B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift in Sources */, 388698622D1986B100008600 /* NotificationRequest.swift in Sources */, 38572CDE2D2254E800B07C69 /* HomePlaceholderViewCell.swift in Sources */, - 380F423A2E88505B009AC59E /* CardUseCaseImpl.swift in Sources */, + 38AE85282EDFF95500029E4C /* LocationUseCaseImpl.swift in Sources */, 38899E582E7936DD0030F7CA /* SooumStyle_V2.swift in Sources */, + 389E59AB2EDEE74B00D0946D /* UploadUserImageUseCase.swift in Sources */, 38FCF41D2EA00625003AC3D8 /* UITtextView.swift in Sources */, 380F42332E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */, 388009972CAC20EC002A9209 /* SOMTags+Rx.swift in Sources */, @@ -3714,7 +3942,7 @@ CODE_SIGN_ENTITLEMENTS = "SOOUM/Resources/SOOUM-Dev.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1020040; + CURRENT_PROJECT_VERSION = 1022030; DEVELOPMENT_TEAM = 99FRG743RX; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SOOUM/Resources/Develop/Info-dev.plist"; @@ -3737,7 +3965,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.20.4; + MARKETING_VERSION = 1.22.3; OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3766,7 +3994,7 @@ CODE_SIGN_ENTITLEMENTS = "SOOUM/Resources/SOOUM-Dev.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1020040; + CURRENT_PROJECT_VERSION = 1022030; DEVELOPMENT_TEAM = 99FRG743RX; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SOOUM/Resources/Develop/Info-dev.plist"; @@ -3789,7 +4017,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.20.4; + MARKETING_VERSION = 1.22.3; OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/SOOUM/SOOUM/Base/BaseViewController.swift b/SOOUM/SOOUM/Base/BaseViewController.swift index 01855183..9b0ef414 100644 --- a/SOOUM/SOOUM/Base/BaseViewController.swift +++ b/SOOUM/SOOUM/Base/BaseViewController.swift @@ -36,11 +36,12 @@ class BaseViewController: UIViewController { private(set) var isEndEditingWhenWillDisappear: Bool = true private(set) var bottomToastMessageOffset: CGFloat = 88 + 8 - override var hidesBottomBarWhenPushed: Bool { - didSet { - NotificationCenter.default.post(name: .hidesBottomBarWhenPushedDidChange, object: self) - } - } + // TODO: 임시, 탭바 숨기지 않음 + // override var hidesBottomBarWhenPushed: Bool { + // didSet { + // NotificationCenter.default.post(name: .hidesBottomBarWhenPushedDidChange, object: self) + // } + // } init() { super.init(nibName: nil, bundle: nil) diff --git a/SOOUM/SOOUM/Base/DIContainer/BaseDIContainer.swift b/SOOUM/SOOUM/Base/DIContainer/BaseDIContainer.swift index 7a1251f5..27f23241 100644 --- a/SOOUM/SOOUM/Base/DIContainer/BaseDIContainer.swift +++ b/SOOUM/SOOUM/Base/DIContainer/BaseDIContainer.swift @@ -29,7 +29,7 @@ protocol BaseDIContainerable: AnyObject { final class BaseDIContainer: BaseDIContainerable { // 부모 컨테이너에 대한 참조입니다. - private let parent: BaseDIContainerable? + private weak var parent: BaseDIContainerable? // 등록된 서비스의 생성 클로저(factory)를 저장하는 딕셔너리입니다. // 키는 서비스 타입의 이름(String), 값은 Any를 반환하는 클로저입니다. private var factories: [String: (BaseDIContainerable) -> Any] = [:] diff --git a/SOOUM/SOOUM/Data/Managers/AuthManager/AuthManager.swift b/SOOUM/SOOUM/Data/Managers/AuthManager/AuthManager.swift index fd177968..a9aa1485 100644 --- a/SOOUM/SOOUM/Data/Managers/AuthManager/AuthManager.swift +++ b/SOOUM/SOOUM/Data/Managers/AuthManager/AuthManager.swift @@ -50,7 +50,7 @@ class AuthManager: CompositeManager { var hasToken: Bool { let token = self.authInfo.token - return !token.accessToken.isEmpty && !token.refreshToken.isEmpty + return token.accessToken.isEmpty == false && token.refreshToken.isEmpty == false } override init(provider: ManagerTypeDelegate, configure: AuthManagerConfiguration) { @@ -187,7 +187,7 @@ extension AuthManager: AuthManagerDelegate { let request: AuthRequest = .login(encryptedDeviceId: encryptedDeviceId) return provider.networkManager.perform(LoginResponse.self, request: request) .map(\.token) - .withUnretained(self) + .withUnretained(object) .flatMapLatest { object, token -> Observable in // session token 업데이트 @@ -235,18 +235,18 @@ extension AuthManager: AuthManagerDelegate { */ func reAuthenticate(_ token: Token, _ completion: @escaping (AuthResult) -> Void) { - guard self.authInfo.token.refreshToken.isEmpty == false else { + guard self.authInfo.token.isEmpty == false else { let error = NSError( domain: "SOOUM", code: -99, - userInfo: [NSLocalizedDescriptionKey: "Refresh token not found"] + userInfo: [NSLocalizedDescriptionKey: "tokens not found"] ) completion(.failure(error)) return } /// 1개 이상의 API에서 reAuthenticate 요청 했을 때, - /// 기존 요청이 끝날 떄까지 대기 + /// 처음 요청이 끝날 떄까지 대기 guard self.isReAuthenticating == false else { self.pendingResults.append(completion) return @@ -270,7 +270,7 @@ extension AuthManager: AuthManagerDelegate { with: self, onNext: { object, token in - if token.accessToken.isEmpty || token.refreshToken.isEmpty { + if token.accessToken.isEmpty && token.refreshToken.isEmpty { let error = NSError( domain: "SOOUM", code: -99, diff --git a/SOOUM/SOOUM/Data/Managers/ManagerProvider.swift b/SOOUM/SOOUM/Data/Managers/ManagerProvider.swift index d14f1359..143b00dd 100644 --- a/SOOUM/SOOUM/Data/Managers/ManagerProvider.swift +++ b/SOOUM/SOOUM/Data/Managers/ManagerProvider.swift @@ -23,13 +23,3 @@ final class ManagerProviderContainer: ManagerProviderType { var networkManager: NetworkManagerDelegate { self.managerType.networkManager } var locationManager: LocationManagerDelegate { self.managerType.locationManager } } - -extension ManagerProviderType { - - func initialize() { - _ = self.authManager - _ = self.pushManager - _ = self.networkManager - _ = self.locationManager - } -} diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/AddingTokenInterceptor.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/AddingTokenInterceptor.swift index 6bc05f39..ae25141e 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/AddingTokenInterceptor.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/AddingTokenInterceptor.swift @@ -5,14 +5,11 @@ // Created by 오현식 on 1/15/25. // -import Foundation - import Alamofire - -class AddingTokenInterceptor: RequestInterceptor { +final class AddingTokenInterceptor: RequestInterceptor { - private let provider: ManagerTypeDelegate + private weak var provider: ManagerTypeDelegate? init(provider: ManagerTypeDelegate) { self.provider = provider @@ -24,11 +21,11 @@ class AddingTokenInterceptor: RequestInterceptor { switch authorizationType { case "access": - let authPayloadForAccess = self.provider.authManager.authPayloadByAccess() + let authPayloadForAccess = self.provider?.authManager.authPayloadByAccess() ?? ["Authorization": "Bearer "] let authKeyForAccess = authPayloadForAccess.keys.first! as String request.setValue(authPayloadForAccess[authKeyForAccess], forHTTPHeaderField: authKeyForAccess) case "refresh": - let authPayloadForRefresh = self.provider.authManager.authPayloadByRefresh() + let authPayloadForRefresh = self.provider?.authManager.authPayloadByRefresh() ?? ["Authorization": "Bearer "] let authKeyForRefresh = authPayloadForRefresh.keys.first! as String request.setValue(authPayloadForRefresh[authKeyForRefresh], forHTTPHeaderField: authKeyForRefresh) default: diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/CompositeInterceptor.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/CompositeInterceptor.swift index c7eacf35..28eed697 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/CompositeInterceptor.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/CompositeInterceptor.swift @@ -5,12 +5,9 @@ // Created by 오현식 on 11/4/24. // -import Foundation - import Alamofire - -class CompositeInterceptor: RequestInterceptor { +final class CompositeInterceptor: RequestInterceptor { private let interceptors: [RequestInterceptor] @@ -43,9 +40,9 @@ class CompositeInterceptor: RequestInterceptor { } } -extension CompositeInterceptor { +private extension CompositeInterceptor { - private func adapts( + func adapts( _ urlRequest: URLRequest, index: Int, session: Session, diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/ErrorInterceptor.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/ErrorInterceptor.swift index 47c5500c..cdaf67d8 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/ErrorInterceptor.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/ErrorInterceptor.swift @@ -7,7 +7,7 @@ import Alamofire -class ErrorInterceptor: RequestInterceptor { +final class ErrorInterceptor: RequestInterceptor { enum Text { static let networkErrorDialogTitle: String = "네트워크 상태가 불안정해요" @@ -30,18 +30,15 @@ class ErrorInterceptor: RequestInterceptor { """ } - private let lock = NSLock() - private let retryLimit: Int = 1 - private let provider: ManagerTypeDelegate + private weak var provider: ManagerTypeDelegate? init(provider: ManagerTypeDelegate) { self.provider = provider } func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping (RetryResult) -> Void) { - self.lock.lock(); defer { self.lock.unlock() } /// API 호출 중 네트워크 오류 발생 if let afError = error.asAFError, @@ -82,8 +79,20 @@ class ErrorInterceptor: RequestInterceptor { return } - let token = self.provider.authManager.authInfo.token - self.provider.authManager.reAuthenticate(token) { result in + guard let provider = self.provider else { + let retryError = NSError( + domain: "SOOUM", + code: -99, + userInfo: [ + NSLocalizedDescriptionKey: "Retry error: `self` deallocated before network response received." + ] + ) + completion(.doNotRetryWithError(retryError)) + return + } + + let token = provider.authManager.authInfo.token + provider.authManager.reAuthenticate(token) { result in switch result { case .success: @@ -150,7 +159,7 @@ class ErrorInterceptor: RequestInterceptor { let subject = Text.inquiryMailTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let guideMessage = """ \(Text.identificationInfo) - \(self.provider.authManager.authInfo.token.refreshToken)\n + \(self.provider?.authManager.authInfo.token.refreshToken ?? "")\n \(Text.inquiryMailGuideMessage) """ let body = guideMessage.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" @@ -180,7 +189,7 @@ class ErrorInterceptor: RequestInterceptor { func goToOnboarding() { - self.provider.authManager.initializeAuthInfo() + self.provider?.authManager.initializeAuthInfo() DispatchQueue.main.async { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/TimeoutInterceptor.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/TimeoutInterceptor.swift index 96283d9e..4e0a3634 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/TimeoutInterceptor.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/Interceptor/TimeoutInterceptor.swift @@ -5,12 +5,9 @@ // Created by 오현식 on 11/4/24. // -import Foundation - import Alamofire - -class TimeoutInterceptor: RequestInterceptor { +final class TimeoutInterceptor: RequestInterceptor { private let timeoutInterval: TimeInterval diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/Monitor/LogginMonitor.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/Monitor/LogginMonitor.swift index 8e80d61b..67da7fd1 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/Monitor/LogginMonitor.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/Monitor/LogginMonitor.swift @@ -5,12 +5,9 @@ // Created by 오현식 on 11/4/24. // -import Foundation - import Alamofire - -class LogginMonitor: EventMonitor { +final class LogginMonitor: EventMonitor { private let formatter: DateFormatter = { let formatter = DateFormatter() diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManager_Version.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManager_Version.swift index 9d023e30..53c8f361 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManager_Version.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManager_Version.swift @@ -21,7 +21,6 @@ extension NetworkManager { return self.fetch(AppVersionStatusResponse.self, request: request) .map { return .success($0) } .catch { return .just(.failure($0)) } - .observe(on: MainScheduler.instance) } func updateCheck() -> Observable { diff --git a/SOOUM/SOOUM/Data/Models/Responses/IsCardDeletedResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/IsCardDeletedResponse.swift new file mode 100644 index 00000000..4f8d811d --- /dev/null +++ b/SOOUM/SOOUM/Data/Models/Responses/IsCardDeletedResponse.swift @@ -0,0 +1,32 @@ +// +// IsCardDeletedResponse.swift +// SOOUM +// +// Created by 오현식 on 12/5/25. +// + +import Alamofire + +struct IsCardDeletedResponse { + + let isDeleted: Bool +} + +extension IsCardDeletedResponse: EmptyResponse { + + static func emptyValue() -> IsCardDeletedResponse { + IsCardDeletedResponse(isDeleted: false) + } +} + +extension IsCardDeletedResponse: Decodable { + + enum CodingKeys: String, CodingKey { + case isDeleted + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.isDeleted = try container.decode(Bool.self, forKey: .isDeleted) + } +} diff --git a/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift index 148de64d..3adcdb2c 100644 --- a/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift @@ -43,6 +43,11 @@ class CardRepositoryImpl: CardRepository { return self.remoteDataSource.detailCard(id: id, latitude: latitude, longitude: longitude) } + func isCardDeleted(id: String) -> Observable { + + return self.remoteDataSource.isCardDeleted(id: id) + } + func commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) -> Observable { return self.remoteDataSource.commentCard(id: id, lastId: lastId, latitude: latitude, longitude: longitude) @@ -130,4 +135,25 @@ class CardRepositoryImpl: CardRepository { tags: tags ) } + + + // MARK: Tag + + func tagCards(tagId: String, lastId: String?) -> Observable { + + return self.remoteDataSource.tagCards(tagId: tagId, lastId: lastId) + } + + + // MARK: My + + func feedCards(userId: String, lastId: String?) -> Observable { + + return self.remoteDataSource.feedCards(userId: userId, lastId: lastId) + } + + func myCommentCards(lastId: String?) -> Observable { + + return self.remoteDataSource.myCommentCards(lastId: lastId) + } } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift index dff00532..79e33bd6 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift @@ -47,6 +47,12 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { return self.provider.networkManager.fetch(DetailCardInfoResponse.self, request: requset) } + func isCardDeleted(id: String) -> Observable { + + let request: CardRequest = .isCardDeleted(id: id) + return self.provider.networkManager.fetch(IsCardDeletedResponse.self, request: request) + } + func commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) -> Observable { let request: CardRequest = .commentCard(id: id, lastId: lastId, latitude: latitude, longitude: longitude) @@ -142,4 +148,28 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { ) return self.provider.networkManager.perform(WriteCardResponse.self, request: request) } + + + // MARK: Tag + + func tagCards(tagId: String, lastId: String?) -> Observable { + + let requset: TagRequest = .tagCards(tagId: tagId, lastId: lastId) + return self.provider.networkManager.fetch(TagCardInfoResponse.self, request: requset) + } + + + // MARK: My + + func feedCards(userId: String, lastId: String?) -> Observable { + + let request: UserRequest = .feedCards(userId: userId, lastId: lastId) + return self.provider.networkManager.fetch(ProfileCardInfoResponse.self, request: request) + } + + func myCommentCards(lastId: String?) -> Observable { + + let request: UserRequest = .myCommentCards(lastId: lastId) + return self.provider.networkManager.fetch(ProfileCardInfoResponse.self, request: request) + } } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift index 7a273ff6..9d072d16 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift @@ -22,6 +22,7 @@ protocol CardRemoteDataSource { // MARK: Detail func detailCard(id: String, latitude: String?, longitude: String?) -> Observable + func isCardDeleted(id: String) -> Observable func commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) -> Observable func deleteCard(id: String) -> Observable func updateLike(id: String, isLike: Bool) -> Observable @@ -55,4 +56,15 @@ protocol CardRemoteDataSource { imgName: String, tags: [String] ) -> Observable + + + // MARK: Tag + + func tagCards(tagId: String, lastId: String?) -> Observable + + + // MARK: My + + func feedCards(userId: String, lastId: String?) -> Observable + func myCommentCards(lastId: String?) -> Observable } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/SettingsRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/SettingsRemoteDataSource.swift index 86e26cba..97e5e1e0 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/SettingsRemoteDataSource.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/SettingsRemoteDataSource.swift @@ -16,4 +16,5 @@ protocol SettingsRemoteDataSource { func enter(code: String, encryptedDeviceId: String) -> Observable func update() -> Observable func blockUsers(lastId: String?) -> Observable + func updateNotify(isAllowNotify: Bool) -> Observable } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/TagRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/TagRemoteDataSource.swift index fd7a54ae..85fe744a 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/TagRemoteDataSource.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/TagRemoteDataSource.swift @@ -13,5 +13,4 @@ protocol TagRemoteDataSource { func favorites() -> Observable func updateFavorite(tagId: String, isFavorite: Bool) -> Observable func ranked() -> Observable - func tagCards(tagId: String, lastId: String?) -> Observable } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/UserRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/UserRemoteDataSource.swift index cd6165a3..a79916f6 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/UserRemoteDataSource.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/UserRemoteDataSource.swift @@ -18,15 +18,11 @@ protocol UserRemoteDataSource { func presignedURL() -> Observable func uploadImage(_ data: Data, with url: URL) -> Observable> func updateImage(imageName: String) -> Observable - func updateFCMToken(fcmToken: String) -> Observable func postingPermission() -> Observable func profile(userId: String?) -> Observable func updateMyProfile(nickname: String?, imageName: String?) -> Observable - func feedCards(userId: String, lastId: String?) -> Observable - func myCommentCards(lastId: String?) -> Observable func followers(userId: String, lastId: String?) -> Observable func followings(userId: String, lastId: String?) -> Observable func updateFollowing(userId: String, isFollow: Bool) -> Observable func updateBlocked(id: String, isBlocked: Bool) -> Observable - func updateNotify(isAllowNotify: Bool) -> Observable } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/SettingsRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/SettingsRemoteDataSourceImpl.swift index f144e56f..81e98ce6 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/SettingsRemoteDataSourceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/SettingsRemoteDataSourceImpl.swift @@ -46,4 +46,10 @@ class SettingsRemoteDataSourceImpl: SettingsRemoteDataSource { let request: SettingsRequest = .blockUsers(lastId: lastId) return self.provider.networkManager.fetch(BlockUsersInfoResponse.self, request: request) } + + func updateNotify(isAllowNotify: Bool) -> Observable { + + let request: UserRequest = .updateNotify(isAllowNotify: isAllowNotify) + return self.provider.networkManager.perform(request) + } } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/TagRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/TagRemoteDataSourceImpl.swift index 99a4939d..26995e52 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/TagRemoteDataSourceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/TagRemoteDataSourceImpl.swift @@ -40,10 +40,4 @@ class TagRemoteDataSourceImpl: TagRemoteDataSource { let request: TagRequest = .ranked return self.provider.networkManager.fetch(TagInfoResponse.self, request: request) } - - func tagCards(tagId: String, lastId: String?) -> Observable { - - let requset: TagRequest = .tagCards(tagId: tagId, lastId: lastId) - return self.provider.networkManager.fetch(TagCardInfoResponse.self, request: requset) - } } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift index 8e26317e..ddb408ae 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift @@ -57,12 +57,6 @@ class UserRemoteDataSourceImpl: UserRemoteDataSource { return self.provider.networkManager.perform(request) } - func updateFCMToken(fcmToken: String) -> Observable { - - let request: UserRequest = .updateFCMToken(fcmToken: fcmToken) - return self.provider.networkManager.perform(request) - } - func postingPermission() -> Observable { let request: UserRequest = .postingPermission @@ -81,18 +75,6 @@ class UserRemoteDataSourceImpl: UserRemoteDataSource { return self.provider.networkManager.perform(request) } - func feedCards(userId: String, lastId: String?) -> Observable { - - let request: UserRequest = .feedCards(userId: userId, lastId: lastId) - return self.provider.networkManager.fetch(ProfileCardInfoResponse.self, request: request) - } - - func myCommentCards(lastId: String?) -> Observable { - - let request: UserRequest = .myCommentCards(lastId: lastId) - return self.provider.networkManager.fetch(ProfileCardInfoResponse.self, request: request) - } - func followers(userId: String, lastId: String?) -> Observable { let request: UserRequest = .followers(userId: userId, lastId: lastId) @@ -116,10 +98,4 @@ class UserRemoteDataSourceImpl: UserRemoteDataSource { let request: UserRequest = .updateBlocked(id: id, isBlocked: isBlocked) return self.provider.networkManager.perform(request) } - - func updateNotify(isAllowNotify: Bool) -> Observable { - - let request: UserRequest = .updateNotify(isAllowNotify: isAllowNotify) - return self.provider.networkManager.perform(request) - } } diff --git a/SOOUM/SOOUM/Data/Repositories/SettingsRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/SettingsRepositoryImpl.swift index eacf6e3b..5e11bad2 100644 --- a/SOOUM/SOOUM/Data/Repositories/SettingsRepositoryImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/SettingsRepositoryImpl.swift @@ -44,6 +44,11 @@ class SettingsRepositoryImpl: SettingsRepository { return self.remoteDataSource.blockUsers(lastId: lastId) } + func updateNotify(isAllowNotify: Bool) -> Observable { + + return self.remoteDataSource.updateNotify(isAllowNotify: isAllowNotify) + } + func notificationStatus() -> Bool { return self.localDataSource.notificationStatus() diff --git a/SOOUM/SOOUM/Data/Repositories/TagRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/TagRepositoryImpl.swift index 5b63e857..41d214ef 100644 --- a/SOOUM/SOOUM/Data/Repositories/TagRepositoryImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/TagRepositoryImpl.swift @@ -36,9 +36,4 @@ class TagRepositoryImpl: TagRepository { return self.remoteDataSource.ranked() } - - func tagCards(tagId: String, lastId: String?) -> Observable { - - return self.remoteDataSource.tagCards(tagId: tagId, lastId: lastId) - } } diff --git a/SOOUM/SOOUM/Data/Repositories/UserRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/UserRepositoryImpl.swift index 793ef7ee..3e34fca6 100644 --- a/SOOUM/SOOUM/Data/Repositories/UserRepositoryImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/UserRepositoryImpl.swift @@ -52,11 +52,6 @@ class UserRepositoryImpl: UserRepository { return self.remoteDataSource.updateImage(imageName: imageName) } - func updateFCMToken(fcmToken: String) -> Observable { - - return self.remoteDataSource.updateFCMToken(fcmToken: fcmToken) - } - func postingPermission() -> Observable { return self.remoteDataSource.postingPermission() @@ -72,16 +67,6 @@ class UserRepositoryImpl: UserRepository { return self.remoteDataSource.updateMyProfile(nickname: nickname, imageName: imageName) } - func feedCards(userId: String, lastId: String?) -> Observable { - - return self.remoteDataSource.feedCards(userId: userId, lastId: lastId) - } - - func myCommentCards(lastId: String?) -> Observable { - - return self.remoteDataSource.myCommentCards(lastId: lastId) - } - func followers(userId: String, lastId: String?) -> Observable { return self.remoteDataSource.followers(userId: userId, lastId: lastId) @@ -101,9 +86,4 @@ class UserRepositoryImpl: UserRepository { return self.remoteDataSource.updateBlocked(id: id, isBlocked: isBlocked) } - - func updateNotify(isAllowNotify: Bool) -> Observable { - - return self.remoteDataSource.updateNotify(isAllowNotify: isAllowNotify) - } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift index 333a0a03..00fecaff 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift @@ -37,6 +37,7 @@ class SOMCard: UIView { $0.contentMode = .scaleAspectFill $0.layer.cornerRadius = 16 $0.layer.borderWidth = 1 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] $0.layer.masksToBounds = true } @@ -60,8 +61,10 @@ class SOMCard: UIView { /// 펑 시간, 거리, 시간, 좋아요 수, 답글 수 정보를 담는 뷰 private let cardInfoContainer = UIView().then { $0.backgroundColor = .som.v2.white - $0.layer.borderColor = UIColor.som.v2.white.cgColor + $0.layer.cornerRadius = 16 $0.layer.borderWidth = 1 + $0.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + $0.layer.masksToBounds = true } /// 펑 시간, 거리, 시간을 담는 스택 뷰 private let cardInfoLeadingStackView = UIStackView().then { @@ -236,11 +239,12 @@ class SOMCard: UIView { // 배경 이미지 뷰 self.addSubview(self.rootContainerImageView) self.rootContainerImageView.snp.makeConstraints { - $0.edges.equalToSuperview() + $0.top.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview().offset(-34) } // 하단 카드 정보 컨테이너 - self.rootContainerImageView.addSubview(self.cardInfoContainer) + self.addSubview(self.cardInfoContainer) self.cardInfoContainer.snp.makeConstraints { $0.bottom.horizontalEdges.equalToSuperview() $0.height.equalTo(34) @@ -319,7 +323,7 @@ class SOMCard: UIView { // 카드 문구 self.rootContainerImageView.addSubview(self.cardTextBackgroundBlurView) self.cardTextBackgroundBlurView.snp.makeConstraints { - $0.centerY.equalToSuperview().offset(-34 * 0.5) + $0.centerY.equalToSuperview() $0.leading.equalToSuperview().offset(32) $0.trailing.equalToSuperview().offset(-32) } @@ -355,9 +359,11 @@ class SOMCard: UIView { func setModel(model: BaseCardInfo) { self.model = model + + let borderColor = model.isAdminCard ? UIColor.som.v2.pMain : UIColor.som.v2.gray100 // 카드 배경 이미지 self.rootContainerImageView.setImage(strUrl: model.cardImgURL, with: model.cardImgName) - self.rootContainerImageView.layer.borderColor = model.isAdminCard ? UIColor.som.v2.pMain.cgColor : UIColor.som.v2.gray100.cgColor + self.rootContainerImageView.layer.borderColor = borderColor.cgColor // 카드 본문 let typography: Typography @@ -373,6 +379,8 @@ class SOMCard: UIView { // 하단 정보 // 어드민, 펑 시간, 거리, 시간 + self.cardInfoContainer.layer.borderColor = borderColor.cgColor + self.adminStackView.isHidden = model.isAdminCard == false self.firstDot.isHidden = model.isAdminCard == false self.cardPungTimeStackView.isHidden = model.storyExpirationTime == nil @@ -384,10 +392,12 @@ class SOMCard: UIView { // 좋아요 수, 답글 수 let likeText = model.likeCnt > 99 ? "99+" : "\(model.likeCnt)" - self.likeLabel.attributedText = .init(string: likeText, attributes: Typography.som.v2.caption2.attributes) + self.likeLabel.text = likeText + self.likeLabel.typography = .som.v2.caption2 let commentText = model.commentCnt > 99 ? "99+" : "\(model.commentCnt)" - self.commentLabel.attributedText = .init(string: commentText, attributes: Typography.som.v2.caption2.attributes) + self.commentLabel.text = commentText + self.commentLabel.typography = .som.v2.caption2 // 스토리 정보 설정 self.subscribePungTime(model.storyExpirationTime) @@ -456,6 +466,7 @@ class SOMCard: UIView { self.cardPungTimeLabel.text = "00 : 00 : 00" self.rootContainerImageView.layer.borderWidth = 0 self.rootContainerImageView.image = UIColor.som.v2.gray200.toImage + self.cardInfoContainer.layer.borderWidth = 0 self.cardInfoContainer.subviews .filter { $0 != self.cardInfoLeadingStackView } .forEach { $0.removeFromSuperview() } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMNicknameTextField.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMNicknameTextField.swift index 0c2347e0..8a8cf141 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMNicknameTextField.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMNicknameTextField.swift @@ -48,6 +48,8 @@ class SOMNicknameTextField: UIView { $0.setContentCompressionResistancePriority(.defaultHigh + 1, for: .vertical) $0.delegate = self + + $0.addTarget(self, action: #selector(self.textDidChanged(_:)), for: .editingChanged) } private let guideMessageContainer = UIStackView().then { @@ -84,7 +86,7 @@ class SOMNicknameTextField: UIView { var text: String? { set { self.textField.text = newValue - self.textField.sendActions(for: .valueChanged) + self.textField.sendActions(for: .editingChanged) } get { return self.textField.text @@ -110,10 +112,6 @@ class SOMNicknameTextField: UIView { } } - var isTextEmpty: Bool { - return self.text?.isEmpty ?? true - } - // MARK: Initalization @@ -158,12 +156,17 @@ class SOMNicknameTextField: UIView { private func clear() { self.clearButton.isHidden = true self.text = nil - self.textField.sendActions(for: .valueChanged) + self.textField.sendActions(for: .editingChanged) if self.textField.isFirstResponder == false { self.textField.becomeFirstResponder() } } + @objc + func textDidChanged(_ textField: UITextField) { + self.clearButton.isHidden = textField.text?.isEmpty ?? true + } + // MARK: Private func @@ -209,11 +212,11 @@ class SOMNicknameTextField: UIView { extension SOMNicknameTextField: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { - self.clearButton.isHidden = self.isTextEmpty + self.clearButton.isHidden = textField.text?.isEmpty ?? true } func textFieldDidEndEditing(_ textField: UITextField) { - self.clearButton.isHidden = self.isTextEmpty + self.clearButton.isHidden = textField.text?.isEmpty ?? true } func textField( diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViews.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViews.swift index 68d1b772..0a163307 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViews.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViews.swift @@ -132,7 +132,10 @@ class SOMPageViews: UIView { offset: .init(width: 0, height: 6) ) - self.hasLayoutsubviews = true + if self.hasLayoutsubviews == false { + self.hasLayoutsubviews = true + self.startAutoScroll() + } } override func didMoveToWindow() { @@ -228,18 +231,16 @@ class SOMPageViews: UIView { snapshot.appendSections(Section.allCases) snapshot.appendItems(modelsToItem, toSection: .main) self.dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in - guard models.count > 1 else { return } + guard let self = self, models.count > 1 else { return } DispatchQueue.main.async { let initialIndexPath: IndexPath = IndexPath(item: 1, section: Section.main.rawValue) - self?.collectionView.scrollToItem( + self.collectionView.scrollToItem( at: initialIndexPath, at: .centeredHorizontally, animated: false ) } - - self?.startAutoScroll() } } @@ -255,14 +256,15 @@ class SOMPageViews: UIView { // 다음 인덱스 (무한 스크롤 배열을 기준으로) let nextIndex = currentIndex + 1 - let nextIndexPath = IndexPath(item: nextIndex, section: Section.main.rawValue) + let targetX = cellWidth * CGFloat(nextIndex) + let targetOffset = CGPoint(x: targetX, y: 0) // 애니메이션과 함께 다음 아이템으로 스크롤 - self.collectionView.scrollToItem( - at: nextIndexPath, - at: .centeredHorizontally, - animated: true - ) + UIView.animate(withDuration: 0.5) { [weak self] in + self?.collectionView.contentOffset = targetOffset + } completion: { _ in + self.infiniteScroll(self.collectionView) + } } } @@ -304,11 +306,6 @@ extension SOMPageViews: UICollectionViewDelegateFlowLayout { self.infiniteScroll(scrollView) self.startAutoScroll() } - /// 자동 스크롤이 끝났을 때, 무한 스크롤 로직 수행 - func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - - self.infiniteScroll(scrollView) - } private func infiniteScroll(_ scrollView: UIScrollView) { diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMPresentationController/SOMAnimationTransitioning.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMPresentationController/SOMAnimationTransitioning.swift index 6261a83b..452a2df8 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMPresentationController/SOMAnimationTransitioning.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMPresentationController/SOMAnimationTransitioning.swift @@ -42,9 +42,9 @@ extension SOMAnimationTransitioning: UIViewControllerAnimatedTransitioning { withDuration: self.transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut] - ) { + ) { [weak self] in - toView.frame.origin.y = containerView.bounds.height - self.initalHeight + toView.frame.origin.y = containerView.bounds.height - (self?.initalHeight ?? 0.0) } completion: { _ in transitionContext.completeTransition(transitionContext.transitionWasCancelled == false) } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift index 33854fac..716500e2 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift @@ -53,6 +53,7 @@ class SOMRefreshControl: UIRefreshControl { override func beginRefreshing() { super.beginRefreshing() + self.animationView.alpha = 1.0 self.animationView.play() self.sendActions(for: .valueChanged) } @@ -78,4 +79,19 @@ class SOMRefreshControl: UIRefreshControl { $0.size.equalTo(44) } } + + + // MARK: Public func + + func updateProgress(offset contentOffsetY: CGFloat, topInset adjustedContentInsetTop: CGFloat) { + + guard self.isRefreshing == false else { return } + + let offset = contentOffsetY + adjustedContentInsetTop + + let threshold: CGFloat = 60 + let alpha = min(max(-offset / threshold, 0.0), 1.0) + + self.animationView.alpha = alpha + } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift index a014b4cd..cf057ed3 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift @@ -216,7 +216,7 @@ class SOMStickyTabBar: UIView { // MARK: Public func - func didSelectTabBarItem(_ index: Int) { + func didSelectTabBarItem(_ index: Int, with animated: Bool = true) { self.tabBarItemContainer.arrangedSubviews.enumerated().forEach { let selectedItem = $1 as? SOMStickyTabBarItem @@ -227,8 +227,9 @@ class SOMStickyTabBar: UIView { let leadingOffset: CGFloat = self.inset.left + (self.spacing * CGFloat(index)) + prevItemWidths self.selectedIndicatorLeadingConstraint?.update(offset: leadingOffset) - UIView.animate(withDuration: 0.25) { - self.layoutIfNeeded() + let duration = animated ? 0.25 : 0 + UIView.animate(withDuration: duration) { [weak self] in + self?.layoutIfNeeded() } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift index 8fe66669..0631ea96 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift @@ -20,6 +20,10 @@ class SOMSwipableTabBar: UIView { static let unSelectedColor: UIColor = UIColor.som.v2.gray400 static let selectedBackgroundColor: UIColor = UIColor.som.v2.gray100 + + enum Text { + static let eventTitle: String = "이벤트" + } } @@ -172,6 +176,7 @@ class SOMSwipableTabBar: UIView { color: index == 0 ? Constants.selectedColor : Constants.unSelectedColor, backgroundColor: index == 0 ? Constants.selectedBackgroundColor : nil ) + item.isEventDotHidden = title != Constants.Text.eventTitle self.tabBarItemContainer.addArrangedSubview(item) } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift index ed867560..d205fd5a 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift @@ -21,6 +21,21 @@ class SOMSwipableTabBarItem: UIView { $0.typography = .som.v2.subtitle3 } + private let dotWithoutReadView = UIView().then { + $0.backgroundColor = .som.v2.rMain + $0.layer.cornerRadius = 5 * 0.5 + $0.isHidden = true + } + + + // MARK: Variables + + var isEventDotHidden: Bool = true { + didSet { + self.dotWithoutReadView.isHidden = self.isEventDotHidden + } + } + // MARK: Initialize @@ -55,6 +70,13 @@ class SOMSwipableTabBarItem: UIView { $0.leading.equalToSuperview().offset(10) $0.trailing.equalToSuperview().offset(-10) } + + self.addSubview(self.dotWithoutReadView) + self.dotWithoutReadView.snp.makeConstraints { + $0.top.equalToSuperview().offset(6) + $0.trailing.equalToSuperview().offset(-3) + $0.size.equalTo(5) + } } func updateState( diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift index 95b0c86c..45dfd04e 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift @@ -72,11 +72,12 @@ class SOMTabBarController: UIViewController { // MARK: Deinitialize deinit { - NotificationCenter.default.removeObserver( - self, - name: .hidesBottomBarWhenPushedDidChange, - object: nil - ) + // TODO: 임시, 탭바 숨기지 않음 + // NotificationCenter.default.removeObserver( + // self, + // name: .hidesBottomBarWhenPushedDidChange, + // object: nil + // ) } @@ -85,12 +86,13 @@ class SOMTabBarController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - NotificationCenter.default.addObserver( - self, - selector: #selector(self.hidesBottomBarWhenPushed(_:)), - name: .hidesBottomBarWhenPushedDidChange, - object: nil - ) + // TODO: 임시, 탭바 숨기지 않음 + // NotificationCenter.default.addObserver( + // self, + // selector: #selector(self.hidesBottomBarWhenPushed(_:)), + // name: .hidesBottomBarWhenPushedDidChange, + // object: nil + // ) self.setupConstraints() } @@ -106,7 +108,6 @@ class SOMTabBarController: UIViewController { } self.view.addSubview(self.tabBar) - self.view.bringSubviewToFront(self.tabBar) self.tabBar.snp.makeConstraints { $0.bottom.horizontalEdges.equalToSuperview() $0.height.equalTo(88) @@ -126,21 +127,22 @@ class SOMTabBarController: UIViewController { // MARK: Objc func - @objc - private func hidesBottomBarWhenPushed(_ notification: Notification) { - - // 탭바컨트롤러의 탭바의 숨김처리를 위해 추가 - guard let viewController = notification.object as? UIViewController, - let selectedViewController = self.selectedViewController as? UINavigationController, - viewController == selectedViewController.topViewController - else { return } - - let hidesTabBar = viewController.hidesBottomBarWhenPushed - UIView.animate(withDuration: 0.25) { - self.messageBubbleView.isHidden = hidesTabBar - self.tabBar.frame.origin.y = hidesTabBar ? self.view.frame.maxY : self.view.frame.maxY - 88 - } - } + // TODO: 임시, 탭바 숨김 애니메이션 제거 + // @objc + // private func hidesBottomBarWhenPushed(_ notification: Notification) { + // + // // 탭바컨트롤러의 탭바의 숨김처리를 위해 추가 + // guard let viewController = notification.object as? UIViewController, + // let selectedViewController = self.selectedViewController as? UINavigationController, + // viewController == selectedViewController.topViewController + // else { return } + // + // let hidesTabBar = viewController.hidesBottomBarWhenPushed + // UIView.animate(withDuration: 0.25) { + // self.messageBubbleView.isHidden = hidesTabBar + // self.tabBar.frame.origin.y = hidesTabBar ? self.view.frame.maxY : self.view.frame.maxY - 88 + // } + // } // MARK: Public func diff --git a/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift b/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift index 4906fc12..660747f6 100644 --- a/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift +++ b/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift @@ -220,6 +220,7 @@ extension UIImage.SOOUMType { case onboarding_finish case check_square_light case detail_delete_card + case guide_write_card case message_tail case placeholder_home case placeholder_notification diff --git a/SOOUM/SOOUM/Domain/Models/DefaultImages.swift b/SOOUM/SOOUM/Domain/Models/DefaultImages.swift index 8996f6de..1fec5afe 100644 --- a/SOOUM/SOOUM/Domain/Models/DefaultImages.swift +++ b/SOOUM/SOOUM/Domain/Models/DefaultImages.swift @@ -14,6 +14,7 @@ struct DefaultImages: Equatable { let food: [ImageUrlInfo] let color: [ImageUrlInfo] let memo: [ImageUrlInfo] + let event: [ImageUrlInfo]? } extension DefaultImages { @@ -24,7 +25,8 @@ extension DefaultImages { sensitivity: [], food: [], color: [], - memo: [] + memo: [], + event: nil ) } @@ -37,6 +39,7 @@ extension DefaultImages: Decodable { case food = "FOOD" case color = "COLOR" case memo = "MEMO" + case event = "EVENT" } init(from decoder: any Decoder) throws { @@ -47,5 +50,6 @@ extension DefaultImages: Decodable { self.food = try container.decode([ImageUrlInfo].self, forKey: .food) self.color = try container.decode([ImageUrlInfo].self, forKey: .color) self.memo = try container.decode([ImageUrlInfo].self, forKey: .memo) + self.event = try container.decodeIfPresent([ImageUrlInfo].self, forKey: .event) } } diff --git a/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift b/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift index d404ae85..dffb98ea 100644 --- a/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift +++ b/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift @@ -19,6 +19,7 @@ struct DetailCardInfo: Hashable { let createdAt: Date let storyExpirationTime: Date? let isAdminCard: Bool + let isReported: Bool let memberId: String let nickname: String let profileImgURL: String? @@ -27,10 +28,8 @@ struct DetailCardInfo: Hashable { let tags: [Tag] let isOwnCard: Bool let visitedCnt: String - /// 답카드 상세보기 - let prevCardId: String? - let isPrevCardDeleted: Bool? - let prevCardImgURL: String? + /// 이전 카드 정보 + let prevCardInfo: PrevCardInfo? } extension DetailCardInfo { @@ -49,6 +48,7 @@ extension DetailCardInfo { createdAt: self.createdAt, storyExpirationTime: self.storyExpirationTime, isAdminCard: self.isAdminCard, + isReported: self.isReported, memberId: self.memberId, nickname: self.nickname, profileImgURL: self.profileImgURL, @@ -57,9 +57,7 @@ extension DetailCardInfo { tags: self.tags, isOwnCard: self.isOwnCard, visitedCnt: self.visitedCnt, - prevCardId: self.prevCardId, - isPrevCardDeleted: self.isPrevCardDeleted, - prevCardImgURL: self.prevCardImgURL + prevCardInfo: self.prevCardInfo ) } } @@ -70,6 +68,12 @@ extension DetailCardInfo { let id: String let title: String } + /// 이전 카드 정보 + struct PrevCardInfo: Hashable { + let prevCardId: String + let isPrevCardDeleted: Bool + let prevCardImgURL: String? + } } extension DetailCardInfo.Tag: Decodable { @@ -86,6 +90,22 @@ extension DetailCardInfo.Tag: Decodable { } } +extension DetailCardInfo.PrevCardInfo: Decodable { + + enum CodingKeys: String, CodingKey { + case prevCardId = "previousCardId" + case isPrevCardDeleted = "isPreviousCardDeleted" + case prevCardImgURL = "previousCardImgUrl" + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.prevCardId = try container.decode(String.self, forKey: .prevCardId) + self.isPrevCardDeleted = try container.decode(Bool.self, forKey: .isPrevCardDeleted) + self.prevCardImgURL = try container.decodeIfPresent(String.self, forKey: .prevCardImgURL) + } +} + extension DetailCardInfo { static var defaultValue: DetailCardInfo = DetailCardInfo( @@ -100,6 +120,7 @@ extension DetailCardInfo { createdAt: Date(), storyExpirationTime: nil, isAdminCard: false, + isReported: false, memberId: "", nickname: "", profileImgURL: nil, @@ -108,9 +129,7 @@ extension DetailCardInfo { tags: [], isOwnCard: false, visitedCnt: "0", - prevCardId: nil, - isPrevCardDeleted: nil, - prevCardImgURL: nil + prevCardInfo: nil ) } @@ -128,6 +147,7 @@ extension DetailCardInfo: Decodable { case createdAt case storyExpirationTime case isAdminCard + case isReported case memberId case nickname case profileImgURL = "profileImgUrl" @@ -136,9 +156,7 @@ extension DetailCardInfo: Decodable { case tags case isOwnCard case visitedCnt - case prevCardId = "previousCardId" - case isPrevCardDeleted = "isPreviousCardDeleted" - case prevCardImgURL = "previousCardImgUrl" + case prevCardInfo } init(from decoder: any Decoder) throws { @@ -154,6 +172,7 @@ extension DetailCardInfo: Decodable { self.createdAt = try container.decode(Date.self, forKey: .createdAt) self.storyExpirationTime = try container.decodeIfPresent(Date.self, forKey: .storyExpirationTime) self.isAdminCard = try container.decode(Bool.self, forKey: .isAdminCard) + self.isReported = try container.decode(Bool.self, forKey: .isReported) self.memberId = String(try container.decode(Int64.self, forKey: .memberId)) self.nickname = try container.decode(String.self, forKey: .nickname) self.profileImgURL = try container.decodeIfPresent(String.self, forKey: .profileImgURL) @@ -162,8 +181,8 @@ extension DetailCardInfo: Decodable { self.tags = try container.decode([Tag].self, forKey: .tags) self.isOwnCard = try container.decode(Bool.self, forKey: .isOwnCard) self.visitedCnt = String(try container.decode(Int64.self, forKey: .visitedCnt)) - self.prevCardId = try container.decodeIfPresent(String.self, forKey: .prevCardId) - self.isPrevCardDeleted = try container.decodeIfPresent(Bool.self, forKey: .isPrevCardDeleted) - self.prevCardImgURL = try container.decodeIfPresent(String.self, forKey: .prevCardImgURL) + + let singleContainer = try decoder.singleValueContainer() + self.prevCardInfo = try? singleContainer.decode(PrevCardInfo.self) } } diff --git a/SOOUM/SOOUM/Domain/Models/Token.swift b/SOOUM/SOOUM/Domain/Models/Token.swift index 97ead7f0..81b7789c 100644 --- a/SOOUM/SOOUM/Domain/Models/Token.swift +++ b/SOOUM/SOOUM/Domain/Models/Token.swift @@ -16,6 +16,10 @@ struct Token: Equatable { extension Token { static var defaultValue: Token = Token(accessToken: "", refreshToken: "") + + var isEmpty: Bool { + return self.accessToken.isEmpty && self.refreshToken.isEmpty + } } extension Token: Decodable { } diff --git a/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift b/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift index f635479f..94d23569 100644 --- a/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift +++ b/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift @@ -22,6 +22,7 @@ protocol CardRepository { // MARK: Detail func detailCard(id: String, latitude: String?, longitude: String?) -> Observable + func isCardDeleted(id: String) -> Observable func commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) -> Observable func deleteCard(id: String) -> Observable func updateLike(id: String, isLike: Bool) -> Observable @@ -55,4 +56,15 @@ protocol CardRepository { imgName: String, tags: [String] ) -> Observable + + + // MARK: Tag + + func tagCards(tagId: String, lastId: String?) -> Observable + + + // MARK: My + + func feedCards(userId: String, lastId: String?) -> Observable + func myCommentCards(lastId: String?) -> Observable } diff --git a/SOOUM/SOOUM/Domain/Repositories/SettingsRepository.swift b/SOOUM/SOOUM/Domain/Repositories/SettingsRepository.swift index a6e312fc..28e7d627 100644 --- a/SOOUM/SOOUM/Domain/Repositories/SettingsRepository.swift +++ b/SOOUM/SOOUM/Domain/Repositories/SettingsRepository.swift @@ -16,6 +16,7 @@ protocol SettingsRepository { func enter(code: String, encryptedDeviceId: String) -> Observable func update() -> Observable func blockUsers(lastId: String?) -> Observable + func updateNotify(isAllowNotify: Bool) -> Observable func notificationStatus() -> Bool func switchNotification(on: Bool) -> Observable diff --git a/SOOUM/SOOUM/Domain/Repositories/TagRepository.swift b/SOOUM/SOOUM/Domain/Repositories/TagRepository.swift index c72bca2e..29563929 100644 --- a/SOOUM/SOOUM/Domain/Repositories/TagRepository.swift +++ b/SOOUM/SOOUM/Domain/Repositories/TagRepository.swift @@ -13,5 +13,4 @@ protocol TagRepository { func favorites() -> Observable func updateFavorite(tagId: String, isFavorite: Bool) -> Observable func ranked() -> Observable - func tagCards(tagId: String, lastId: String?) -> Observable } diff --git a/SOOUM/SOOUM/Domain/Repositories/UserRepository.swift b/SOOUM/SOOUM/Domain/Repositories/UserRepository.swift index 4a46a6d4..f3c555c6 100644 --- a/SOOUM/SOOUM/Domain/Repositories/UserRepository.swift +++ b/SOOUM/SOOUM/Domain/Repositories/UserRepository.swift @@ -18,15 +18,11 @@ protocol UserRepository { func presignedURL() -> Observable func uploadImage(_ data: Data, with url: URL) -> Observable> func updateImage(imageName: String) -> Observable - func updateFCMToken(fcmToken: String) -> Observable func postingPermission() -> Observable func profile(userId: String?) -> Observable func updateMyProfile(nickname: String?, imageName: String?) -> Observable - func feedCards(userId: String, lastId: String?) -> Observable - func myCommentCards(lastId: String?) -> Observable func followers(userId: String, lastId: String?) -> Observable func followings(userId: String, lastId: String?) -> Observable func updateFollowing(userId: String, isFollow: Bool) -> Observable func updateBlocked(id: String, isBlocked: Bool) -> Observable - func updateNotify(isAllowNotify: Bool) -> Observable } diff --git a/SOOUM/SOOUM/Domain/UseCases/AppVersionUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/AppVersionUseCaseImpl.swift index 1b45c626..67c19fcd 100644 --- a/SOOUM/SOOUM/Domain/UseCases/AppVersionUseCaseImpl.swift +++ b/SOOUM/SOOUM/Domain/UseCases/AppVersionUseCaseImpl.swift @@ -5,11 +5,9 @@ // Created by 오현식 on 9/16/25. // -import Foundation - import RxSwift -class AppVersionUseCaseImpl: AppVersionUseCase { +final class AppVersionUseCaseImpl: AppVersionUseCase { private let repository: AppVersionRepository @@ -19,6 +17,6 @@ class AppVersionUseCaseImpl: AppVersionUseCase { func version() -> Observable { - return self.repository.version().map { $0.version } + return self.repository.version().map(\.version) } } diff --git a/SOOUM/SOOUM/Domain/UseCases/AuthUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/AuthUseCaseImpl.swift index f71b17dd..fcbe3b9d 100644 --- a/SOOUM/SOOUM/Domain/UseCases/AuthUseCaseImpl.swift +++ b/SOOUM/SOOUM/Domain/UseCases/AuthUseCaseImpl.swift @@ -5,11 +5,9 @@ // Created by 오현식 on 9/17/25. // -import Foundation - import RxSwift -class AuthUseCaseImpl: AuthUseCase { +final class AuthUseCaseImpl: AuthUseCase { private let repository: AuthRepository @@ -32,6 +30,11 @@ class AuthUseCaseImpl: AuthUseCase { return self.repository.withdraw(reaseon: reaseon).map { $0 == 200 } } + func encryptedDeviceId() -> Observable { + + return self.repository.encryptedDeviceId() + } + func initializeAuthInfo() { self.repository.initializeAuthInfo() @@ -46,9 +49,4 @@ class AuthUseCaseImpl: AuthUseCase { return self.repository.tokens() } - - func encryptedDeviceId() -> Observable { - - return self.repository.encryptedDeviceId() - } } diff --git a/SOOUM/SOOUM/Domain/UseCases/BlockUserUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/BlockUserUseCaseImpl.swift new file mode 100644 index 00000000..3d29a7f2 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/BlockUserUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// BlockUserUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class BlockUserUseCaseImpl: BlockUserUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func updateBlocked(userId: String, isBlocked: Bool) -> Observable { + + return self.repository.updateBlocked(id: userId, isBlocked: isBlocked).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/CardImageUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/CardImageUseCaseImpl.swift new file mode 100644 index 00000000..a05fac2a --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/CardImageUseCaseImpl.swift @@ -0,0 +1,32 @@ +// +// CardImageUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class CardImageUseCaseImpl: CardImageUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func defaultImages() -> Observable { + + return self.repository.defaultImages().map { $0.defaultImages } + } + + func presignedURL() -> Observable { + + return self.repository.presignedURL().map(\.imageUrlInfo) + } + + func uploadToS3(_ data: Data, with url: URL) -> Observable { + + return self.repository.uploadImage(data, with: url).map { (try? $0.get()) == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift deleted file mode 100644 index a7685e61..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// CardUseCaseImpl.swift -// SOOUM -// -// Created by 오현식 on 9/28/25. -// - -import Foundation - -import RxSwift - -class CardUseCaseImpl: CardUseCase { - - private let repository: CardRepository - - init(repository: CardRepository) { - self.repository = repository - } - - - // MARK: Home - - func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> { - - return self.repository.latestCard(lastId: lastId, latitude: latitude, longitude: longitude).map { $0.cardInfos } - } - - func popularCard(latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> { - - return self.repository.popularCard(latitude: latitude, longitude: longitude).map { $0.cardInfos } - } - - func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable<[BaseCardInfo]> { - - return self.repository.distanceCard(lastId: lastId, latitude: latitude, longitude: longitude, distanceFilter: distanceFilter).map { $0.cardInfos } - } - - - // MARK: Detail - - func detailCard(id: String, latitude: String?, longitude: String?) -> Observable { - - return self.repository.detailCard(id: id, latitude: latitude, longitude: longitude).map { $0.cardInfos } - } - - func commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> { - - return self.repository.commentCard(id: id, lastId: lastId, latitude: latitude, longitude: longitude).map { $0.cardInfos } - } - - func deleteCard(id: String) -> Observable { - - return self.repository.deleteCard(id: id).map { $0 == 200 } - } - - func updateLike(id: String, isLike: Bool) -> Observable { - - return self.repository.updateLike(id: id, isLike: isLike).map { $0 == 200 } - } - - func reportCard(id: String, reportType: String) -> Observable { - - return self.repository.reportCard(id: id, reportType: reportType).map { $0 == 200 } - } - - - // MARK: Write - - func defaultImages() -> Observable { - - return self.repository.defaultImages().map { $0.defaultImages } - } - - func presignedURL() -> Observable { - - return self.repository.presignedURL().map { $0.imageUrlInfo } - } - - func uploadImage(_ data: Data, with url: URL) -> Observable { - - return self.repository.uploadImage(data, with: url) - .map { (try? $0.get()) == 200 } - } - - func writeCard( - isDistanceShared: Bool, - latitude: String?, - longitude: String?, - content: String, - font: String, - imgType: String, - imgName: String, - isStory: Bool, - tags: [String] - ) -> Observable { - - return self.repository.writeCard( - isDistanceShared: isDistanceShared, - latitude: latitude, - longitude: longitude, - content: content, - font: font, - imgType: imgType, - imgName: imgName, - isStory: isStory, - tags: tags - ) - .map { $0.cardId } - } - - func writeComment( - id: String, - isDistanceShared: Bool, - latitude: String?, - longitude: String?, - content: String, - font: String, - imgType: String, - imgName: String, - tags: [String] - ) -> Observable { - - return self.repository.writeComment( - id: id, - isDistanceShared: isDistanceShared, - latitude: latitude, - longitude: longitude, - content: content, - font: font, - imgType: imgType, - imgName: imgName, - tags: tags - ) - .map { $0.cardId } - } -} diff --git a/SOOUM/SOOUM/Domain/UseCases/DeleteCardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/DeleteCardUseCaseImpl.swift new file mode 100644 index 00000000..c195f98e --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/DeleteCardUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// DeleteCardUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class DeleteCardUseCaseImpl: DeleteCardUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func delete(cardId: String) -> Observable { + + return self.repository.deleteCard(id: cardId).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchBlockUserUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchBlockUserUseCaseImpl.swift new file mode 100644 index 00000000..565d789f --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchBlockUserUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// FetchBlockUserUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchBlockUserUseCaseImpl: FetchBlockUserUseCase { + + private let repository: SettingsRepository + + init(repository: SettingsRepository) { + self.repository = repository + } + + func blockUsers(lastId: String?) -> Observable<[BlockUserInfo]> { + + return self.repository.blockUsers(lastId: lastId).map(\.blockUsers) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchCardDetailUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchCardDetailUseCaseImpl.swift new file mode 100644 index 00000000..45562f87 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchCardDetailUseCaseImpl.swift @@ -0,0 +1,52 @@ +// +// FetchCardDetailUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchCardDetailUseCaseImpl: FetchCardDetailUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func detailCard( + id: String, + latitude: String?, + longitude: String? + ) -> Observable { + + return self.repository.detailCard( + id: id, + latitude: latitude, + longitude: longitude + ) + .map(\.cardInfos) + } + + func commentCards( + id: String, + lastId: String?, + latitude: String?, + longitude: String? + ) -> Observable<[BaseCardInfo]> { + + return self.repository.commentCard( + id: id, + lastId: lastId, + latitude: latitude, + longitude: longitude + ) + .map(\.cardInfos) + } + + func isDeleted(cardId: String) -> Observable { + + return self.repository.isCardDeleted(id: cardId).map(\.isDeleted) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchCardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchCardUseCaseImpl.swift new file mode 100644 index 00000000..41cac4af --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchCardUseCaseImpl.swift @@ -0,0 +1,84 @@ +// +// FetchCardUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchCardUseCaseImpl: FetchCardUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + /// 홈 피드 카드 조회 최신/인기/거리 + func latestCards( + lastId: String?, + latitude: String?, + longitude: String? + ) -> Observable<[BaseCardInfo]> { + + return self.repository.latestCard( + lastId: lastId, + latitude: latitude, + longitude: longitude + ) + .map(\.cardInfos) + } + + func popularCards( + latitude: String?, + longitude: String? + ) -> Observable<[BaseCardInfo]> { + + return self.repository.popularCard( + latitude: latitude, + longitude: longitude + ) + .map(\.cardInfos) + } + + func distanceCards( + lastId: String?, + latitude: String, + longitude: String, + distanceFilter: String + ) -> Observable<[BaseCardInfo]> { + + return self.repository.distanceCard( + lastId: lastId, + latitude: latitude, + longitude: longitude, + distanceFilter: distanceFilter + ) + .map(\.cardInfos) + } + + /// 마이 카드 조회 피드/댓글 + func writtenFeedCards(userId: String, lastId: String?) -> Observable<[ProfileCardInfo]> { + + return self.repository.feedCards(userId: userId, lastId: lastId).map(\.cardInfos) + } + + func writtenCommentCards(lastId: String?) -> Observable<[ProfileCardInfo]> { + + return self.repository.myCommentCards(lastId: lastId).map(\.cardInfos) + } + + /// 태그 태그가 포함된 카드 조회 + func cardsWithTag( + tagId: String, + lastId: String? + ) -> Observable<(cardInfos: [ProfileCardInfo], isFavorite: Bool)> { + + return self.repository.tagCards( + tagId: tagId, + lastId: lastId + ) + .map { ($0.cardInfos, $0.isFavorite) } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchFollowUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchFollowUseCaseImpl.swift new file mode 100644 index 00000000..0edc960a --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchFollowUseCaseImpl.swift @@ -0,0 +1,27 @@ +// +// FetchFollowUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchFollowUseCaseImpl: FetchFollowUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func followers(userId: String, lastId: String?) -> Observable<[FollowInfo]> { + + return self.repository.followers(userId: userId, lastId: lastId).map(\.followInfos) + } + + func followings(userId: String, lastId: String?) -> Observable<[FollowInfo]> { + + return self.repository.followings(userId: userId, lastId: lastId).map(\.followInfos) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchNoticeUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchNoticeUseCaseImpl.swift new file mode 100644 index 00000000..38061b05 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchNoticeUseCaseImpl.swift @@ -0,0 +1,31 @@ +// +// FetchNoticeUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchNoticeUseCaseImpl: FetchNoticeUseCase { + + private let repository: NotificationRepository + + init(repository: NotificationRepository) { + self.repository = repository + } + + func notices( + lastId: String?, + size: Int, + requestType: NotificationRequest.RequestType + ) -> Observable<[NoticeInfo]> { + + return self.repository.notices( + lastId: lastId, + size: size, + requestType: requestType + ) + .map(\.noticeInfos) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchTagUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchTagUseCaseImpl.swift new file mode 100644 index 00000000..6b721f6f --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchTagUseCaseImpl.swift @@ -0,0 +1,47 @@ +// +// FetchTagUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchTagUseCaseImpl: FetchTagUseCase { + + private let repository: TagRepository + + init(repository: TagRepository) { + self.repository = repository + } + + func related(keyword: String, size: Int) -> Observable<[TagInfo]> { + + return self.repository.related(keyword: keyword, size: size).map(\.tagInfos) + } + + /// 관심 태그는 최대 9개 + func favorites() -> Observable<[FavoriteTagInfo]> { + + return self.repository.favorites().map(\.tagInfos).map { Array($0.prefix(9)) } + } + + func isFavorites(with tagInfo: FavoriteTagInfo) -> Observable { + + return self.favorites().map { $0.contains(tagInfo) } + } + + // 인기 태그는 최소 1개 이상일 때 표시 + // 인기 태그는 최대 10개까지 표시 + func ranked() -> Observable<[TagInfo]> { + + return self.repository.ranked() + .map(\.tagInfos) + .map { $0.filter { $0.usageCnt > 0 } } + // 중복 제거 + // .map { Array(Set($0)) } + // 태그 갯수로 정렬 + // .map { $0.sorted(by: { $0.usageCnt > $1.usageCnt }) } + .map { Array($0.prefix(10)) } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/FetchUserInfoUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/FetchUserInfoUseCaseImpl.swift new file mode 100644 index 00000000..e87c02e5 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/FetchUserInfoUseCaseImpl.swift @@ -0,0 +1,27 @@ +// +// FetchUserInfoUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class FetchUserInfoUseCaseImpl: FetchUserInfoUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func userInfo(userId: String?) -> Observable { + + return self.repository.profile(userId: userId).map(\.profileInfo) + } + + func myNickname() -> Observable { + + return self.userInfo(userId: nil).map(\.nickname) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/AppVersionUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/AppVersionUseCase.swift index 08e0015f..11e48fb1 100644 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/AppVersionUseCase.swift +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/AppVersionUseCase.swift @@ -5,11 +5,9 @@ // Created by 오현식 on 9/16/25. // -import Foundation - import RxSwift -protocol AppVersionUseCase { +protocol AppVersionUseCase: AnyObject { func version() -> Observable } diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/AuthUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/AuthUseCase.swift index 561e70fa..249900d5 100644 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/AuthUseCase.swift +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/AuthUseCase.swift @@ -5,18 +5,17 @@ // Created by 오현식 on 9/17/25. // -import Foundation - import RxSwift -protocol AuthUseCase { +protocol AuthUseCase: AnyObject { func signUp(nickname: String, profileImageName: String?) -> Observable func login() -> Observable func withdraw(reaseon: String) -> Observable + func encryptedDeviceId() -> Observable + func initializeAuthInfo() func hasToken() -> Bool func tokens() -> Token - func encryptedDeviceId() -> Observable } diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/BlockUserUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/BlockUserUseCase.swift new file mode 100644 index 00000000..381e5a9f --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/BlockUserUseCase.swift @@ -0,0 +1,13 @@ +// +// BlockUserUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol BlockUserUseCase: AnyObject { + + func updateBlocked(userId: String, isBlocked: Bool) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardImageUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardImageUseCase.swift new file mode 100644 index 00000000..d9b24bc7 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardImageUseCase.swift @@ -0,0 +1,15 @@ +// +// CardImageUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol CardImageUseCase: AnyObject { + + func defaultImages() -> Observable + func presignedURL() -> Observable + func uploadToS3(_ data: Data, with url: URL) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift deleted file mode 100644 index b873ad91..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// CardUseCase.swift -// SOOUM -// -// Created by 오현식 on 9/28/25. -// - -import Foundation - -import RxSwift - -protocol CardUseCase { - - - // MARK: Home - - func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> - func popularCard(latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> - func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable<[BaseCardInfo]> - - - // MARK: Detail - - func detailCard(id: String, latitude: String?, longitude: String?) -> Observable - func commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> - func deleteCard(id: String) -> Observable - func updateLike(id: String, isLike: Bool) -> Observable - func reportCard(id: String, reportType: String) -> Observable - - - // MARK: Write - - func defaultImages() -> Observable - func presignedURL() -> Observable - func uploadImage(_ data: Data, with url: URL) -> Observable - func writeCard( - isDistanceShared: Bool, - latitude: String?, - longitude: String?, - content: String, - font: String, - imgType: String, - imgName: String, - isStory: Bool, - tags: [String] - ) -> Observable - func writeComment( - id: String, - isDistanceShared: Bool, - latitude: String?, - longitude: String?, - content: String, - font: String, - imgType: String, - imgName: String, - tags: [String] - ) -> Observable -} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/DeleteCardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/DeleteCardUseCase.swift new file mode 100644 index 00000000..bfe55232 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/DeleteCardUseCase.swift @@ -0,0 +1,13 @@ +// +// DeleteCardUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol DeleteCardUseCase: AnyObject { + + func delete(cardId: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchBlockUserUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchBlockUserUseCase.swift new file mode 100644 index 00000000..1325039a --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchBlockUserUseCase.swift @@ -0,0 +1,13 @@ +// +// FetchBlockUserUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchBlockUserUseCase: AnyObject { + + func blockUsers(lastId: String?) -> Observable<[BlockUserInfo]> +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchCardDetailUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchCardDetailUseCase.swift new file mode 100644 index 00000000..653e488b --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchCardDetailUseCase.swift @@ -0,0 +1,26 @@ +// +// FetchCardDetailUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchCardDetailUseCase: AnyObject { + + func detailCard( + id: String, + latitude: String?, + longitude: String? + ) -> Observable + + func commentCards( + id: String, + lastId: String?, + latitude: String?, + longitude: String? + ) -> Observable<[BaseCardInfo]> + + func isDeleted(cardId: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchCardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchCardUseCase.swift new file mode 100644 index 00000000..c9545dd9 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchCardUseCase.swift @@ -0,0 +1,38 @@ +// +// FetchCardUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchCardUseCase: AnyObject { + + /// 홈 피드 카드 조회 최신/인기/거리 + func latestCards( + lastId: String?, + latitude: String?, + longitude: String? + ) -> Observable<[BaseCardInfo]> + func popularCards( + latitude: String?, + longitude: String? + ) -> Observable<[BaseCardInfo]> + func distanceCards( + lastId: String?, + latitude: String, + longitude: String, + distanceFilter: String + ) -> Observable<[BaseCardInfo]> + + /// 마이 카드 조회 피드/댓글 + func writtenFeedCards(userId: String, lastId: String?) -> Observable<[ProfileCardInfo]> + func writtenCommentCards(lastId: String?) -> Observable<[ProfileCardInfo]> + + /// 태그 태그가 포함된 카드 조회 + func cardsWithTag( + tagId: String, + lastId: String? + ) -> Observable<(cardInfos: [ProfileCardInfo], isFavorite: Bool)> +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchFollowUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchFollowUseCase.swift new file mode 100644 index 00000000..ca497f0f --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchFollowUseCase.swift @@ -0,0 +1,14 @@ +// +// FetchFollowUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchFollowUseCase: AnyObject { + + func followers(userId: String, lastId: String?) -> Observable<[FollowInfo]> + func followings(userId: String, lastId: String?) -> Observable<[FollowInfo]> +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchNoticeUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchNoticeUseCase.swift new file mode 100644 index 00000000..ba21e10a --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchNoticeUseCase.swift @@ -0,0 +1,17 @@ +// +// FetchNoticeUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchNoticeUseCase: AnyObject { + + func notices( + lastId: String?, + size: Int, + requestType: NotificationRequest.RequestType + ) -> Observable<[NoticeInfo]> +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchTagUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchTagUseCase.swift new file mode 100644 index 00000000..5b44c07b --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchTagUseCase.swift @@ -0,0 +1,16 @@ +// +// FetchTagUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchTagUseCase: AnyObject { + + func related(keyword: String, size: Int) -> Observable<[TagInfo]> + func favorites() -> Observable<[FavoriteTagInfo]> + func isFavorites(with tagInfo: FavoriteTagInfo) -> Observable + func ranked() -> Observable<[TagInfo]> +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchUserInfoUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchUserInfoUseCase.swift new file mode 100644 index 00000000..8b89ef82 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/FetchUserInfoUseCase.swift @@ -0,0 +1,14 @@ +// +// FetchUserInfoUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol FetchUserInfoUseCase: AnyObject { + + func userInfo(userId: String?) -> Observable + func myNickname() -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/LocationUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/LocationUseCase.swift new file mode 100644 index 00000000..fbce5e0d --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/LocationUseCase.swift @@ -0,0 +1,16 @@ +// +// LocationUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol LocationUseCase: AnyObject { + + func coordinate() -> Coordinate + func hasPermission() -> Bool + func requestLocationPermission() + func checkLocationAuthStatus() -> AuthStatus +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift index 98fb3339..f0ca1575 100644 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift @@ -5,14 +5,12 @@ // Created by 오현식 on 9/17/25. // -import Foundation - import RxSwift -protocol NotificationUseCase { +protocol NotificationUseCase: AnyObject { func unreadNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> func readNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> + func isUnreadNotiEmpty() -> Observable func requestRead(notificationId: String) -> Observable - func notices(lastId: String?, size: Int?, requestType: NotificationRequest.RequestType) -> Observable<[NoticeInfo]> } diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/ReportCardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/ReportCardUseCase.swift new file mode 100644 index 00000000..855d890a --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/ReportCardUseCase.swift @@ -0,0 +1,13 @@ +// +// ReportCardUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol ReportCardUseCase: AnyObject { + + func report(cardId: String, reportType: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/SettingsUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/SettingsUseCase.swift deleted file mode 100644 index 2480201b..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/SettingsUseCase.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SettingsUseCase.swift -// SOOUM -// -// Created by 오현식 on 11/9/25. -// - -import Foundation - -import RxSwift - -protocol SettingsUseCase { - - func rejoinableDate() -> Observable - func issue() -> Observable - func enter(code: String, encryptedDeviceId: String) -> Observable - func update() -> Observable - func blockUsers(lastId: String?) -> Observable<[BlockUserInfo]> - - func notificationStatus() -> Bool - func switchNotification(on: Bool) -> Observable - - func coordinate() -> Coordinate - func hasPermission() -> Bool - func requestLocationPermission() - func checkLocationAuthStatus() -> AuthStatus -} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/TagUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/TagUseCase.swift deleted file mode 100644 index 5c50830d..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/TagUseCase.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TagUseCase.swift -// SOOUM -// -// Created by 오현식 on 10/8/25. -// - -import RxSwift - -protocol TagUseCase { - - func related(keyword: String, size: Int) -> Observable<[TagInfo]> - func favorites() -> Observable<[FavoriteTagInfo]> - func updateFavorite(tagId: String, isFavorite: Bool) -> Observable - func ranked() -> Observable<[TagInfo]> - func tagCards(tagId: String, lastId: String?) -> Observable<(cardInfos: [ProfileCardInfo], isFavorite: Bool)> -} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/TransferAccountUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/TransferAccountUseCase.swift new file mode 100644 index 00000000..79afdad2 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/TransferAccountUseCase.swift @@ -0,0 +1,15 @@ +// +// TransferAccountUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol TransferAccountUseCase: AnyObject { + + func issue() -> Observable + func update() -> Observable + func enter(code: String, encryptedDeviceId: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateCardLikeUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateCardLikeUseCase.swift new file mode 100644 index 00000000..caaecc93 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateCardLikeUseCase.swift @@ -0,0 +1,13 @@ +// +// UpdateCardLikeUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol UpdateCardLikeUseCase: AnyObject { + + func updateLike(cardId: String, isLike: Bool) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateFollowUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateFollowUseCase.swift new file mode 100644 index 00000000..db4be51e --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateFollowUseCase.swift @@ -0,0 +1,13 @@ +// +// UpdateFollowUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol UpdateFollowUseCase: AnyObject { + + func updateFollowing(userId: String, isFollow: Bool) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateNotifyUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateNotifyUseCase.swift new file mode 100644 index 00000000..6b1fa516 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateNotifyUseCase.swift @@ -0,0 +1,16 @@ +// +// UpdateNotifyUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol UpdateNotifyUseCase: AnyObject { + + func notificationStatus() -> Bool + func switchNotification(on: Bool) -> Observable + + func updateNotify(isAllowNotify: Bool) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateTagFavoriteUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateTagFavoriteUseCase.swift new file mode 100644 index 00000000..04d35e66 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateTagFavoriteUseCase.swift @@ -0,0 +1,13 @@ +// +// UpdateTagFavoriteUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol UpdateTagFavoriteUseCase: AnyObject { + + func updateFavorite(tagId: String, isFavorite: Bool) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateUserInfoUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateUserInfoUseCase.swift new file mode 100644 index 00000000..1a3c0cee --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UpdateUserInfoUseCase.swift @@ -0,0 +1,13 @@ +// +// UpdateUserInfoUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol UpdateUserInfoUseCase: AnyObject { + + func updateUserInfo(nickname: String?, imageName: String?) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UploadUserImageUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UploadUserImageUseCase.swift new file mode 100644 index 00000000..7ebc5571 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UploadUserImageUseCase.swift @@ -0,0 +1,15 @@ +// +// UploadUserImageUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol UploadUserImageUseCase: AnyObject { + + func presignedURL() -> Observable + func uploadToS3(_ data: Data, with url: URL) -> Observable + func registerImageName(imageName: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UserUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/UserUseCase.swift deleted file mode 100644 index 4ba3f45b..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/UserUseCase.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// UserUseCase.swift -// SOOUM -// -// Created by 오현식 on 9/17/25. -// - -import Foundation - -import RxSwift - -protocol UserUseCase { - - func isAvailableCheck() -> Observable - func nickname() -> Observable - func isNicknameValid(nickname: String) -> Observable - func updateNickname(nickname: String) -> Observable - func presignedURL() -> Observable - func uploadImage(_ data: Data, with url: URL) -> Observable - func updateImage(imageName: String) -> Observable - func updateFCMToken(fcmToken: String) -> Observable - func postingPermission() -> Observable - func profile(userId: String?) -> Observable - func updateMyProfile(nickname: String?, imageName: String?) -> Observable - func feedCards(userId: String, lastId: String?) -> Observable<[ProfileCardInfo]> - func myCommentCards(lastId: String?) -> Observable<[ProfileCardInfo]> - func followers(userId: String, lastId: String?) -> Observable<[FollowInfo]> - func followings(userId: String, lastId: String?) -> Observable<[FollowInfo]> - func updateFollowing(userId: String, isFollow: Bool) -> Observable - func updateBlocked(id: String, isBlocked: Bool) -> Observable - func updateNotify(isAllowNotify: Bool) -> Observable -} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/ValidateNicknameUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/ValidateNicknameUseCase.swift new file mode 100644 index 00000000..d34ea706 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/ValidateNicknameUseCase.swift @@ -0,0 +1,14 @@ +// +// ValidateNicknameUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol ValidateNicknameUseCase: AnyObject { + + func nickname() -> Observable + func checkValidation(nickname: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/ValidateUserUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/ValidateUserUseCase.swift new file mode 100644 index 00000000..586b5bd3 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/ValidateUserUseCase.swift @@ -0,0 +1,15 @@ +// +// ValidateUserUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol ValidateUserUseCase: AnyObject { + + func checkValidation() -> Observable + func iswithdrawn() -> Observable + func postingPermission() -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/WriteCardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/WriteCardUseCase.swift new file mode 100644 index 00000000..84592bfd --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/WriteCardUseCase.swift @@ -0,0 +1,34 @@ +// +// WriteCardUseCase.swift +// SOOUM +// +// Created by 오현식 on 12/2/25. +// + +import RxSwift + +protocol WriteCardUseCase: AnyObject { + + func writeFeed( + isDistanceShared: Bool, + latitude: String?, + longitude: String?, + content: String, + font: String, + imgType: String, + imgName: String, + isStory: Bool, + tags: [String] + ) -> Observable + func writeComment( + parentCardId: String, + isDistanceShared: Bool, + latitude: String?, + longitude: String?, + content: String, + font: String, + imgType: String, + imgName: String, + tags: [String] + ) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/UseCases/LocationUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/LocationUseCaseImpl.swift new file mode 100644 index 00000000..9e99c8d8 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/LocationUseCaseImpl.swift @@ -0,0 +1,37 @@ +// +// LocationUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class LocationUseCaseImpl: LocationUseCase { + + private let repository: SettingsRepository + + init(repository: SettingsRepository) { + self.repository = repository + } + + func coordinate() -> Coordinate { + + return self.repository.coordinate() + } + + func hasPermission() -> Bool { + + return self.repository.hasPermission() + } + + func requestLocationPermission() { + + self.repository.requestLocationPermission() + } + + func checkLocationAuthStatus() -> AuthStatus { + + return self.repository.checkLocationAuthStatus() + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift index 68aeb99d..7f432815 100644 --- a/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift +++ b/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift @@ -5,8 +5,6 @@ // Created by 오현식 on 9/17/25. // -import Foundation - import RxSwift class NotificationUseCaseImpl: NotificationUseCase { @@ -19,21 +17,21 @@ class NotificationUseCaseImpl: NotificationUseCase { func unreadNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> { - return self.repository.unreadNotifications(lastId: lastId).map { $0.notificationInfo } + return self.repository.unreadNotifications(lastId: lastId).map(\.notificationInfo) } func readNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> { - return self.repository.readNotifications(lastId: lastId).map { $0.notificationInfo } + return self.repository.readNotifications(lastId: lastId).map(\.notificationInfo) } - func requestRead(notificationId: String) -> Observable { + func isUnreadNotiEmpty() -> Observable { - return self.repository.requestRead(notificationId: notificationId).map { $0 == 200 } + return self.unreadNotifications(lastId: nil).map(\.isEmpty) } - func notices(lastId: String?, size: Int?, requestType: NotificationRequest.RequestType) -> Observable<[NoticeInfo]> { + func requestRead(notificationId: String) -> Observable { - return self.repository.notices(lastId: lastId, size: size, requestType: requestType).map { $0.noticeInfos } + return self.repository.requestRead(notificationId: notificationId).map { $0 == 200 } } } diff --git a/SOOUM/SOOUM/Domain/UseCases/ReportCardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/ReportCardUseCaseImpl.swift new file mode 100644 index 00000000..114edc89 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/ReportCardUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// ReportCardUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class ReportCardUseCaseImpl: ReportCardUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func report(cardId: String, reportType: String) -> Observable { + + return self.repository.reportCard(id: cardId, reportType: reportType).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/SettingsUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/SettingsUseCaseImpl.swift deleted file mode 100644 index 8ce4fdcf..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/SettingsUseCaseImpl.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// SettingsUseCaseImpl.swift -// SOOUM -// -// Created by 오현식 on 11/9/25. -// - -import Foundation - -import RxSwift - -class SettingsUseCaseImpl: SettingsUseCase { - - private let repository: SettingsRepository - - init(repository: SettingsRepository) { - self.repository = repository - } - - func rejoinableDate() -> Observable { - - return self.repository.rejoinableDate().map(\.rejoinableDate) - } - - func issue() -> Observable { - - return self.repository.issue().map(\.transferInfo) - } - - func enter(code: String, encryptedDeviceId: String) -> Observable { - - return self.repository.enter(code: code, encryptedDeviceId: encryptedDeviceId).map { $0 == 200 } - } - - func update() -> Observable { - - return self.repository.update().map(\.transferInfo) - } - - func blockUsers(lastId: String?) -> Observable<[BlockUserInfo]> { - - return self.repository.blockUsers(lastId: lastId).map(\.blockUsers) - } - - func notificationStatus() -> Bool { - - return self.repository.notificationStatus() - } - - func switchNotification(on: Bool) -> Observable { - - return self.repository.switchNotification(on: on).map { _ in () } - } - - func coordinate() -> Coordinate { - - return self.repository.coordinate() - } - - func hasPermission() -> Bool { - - return self.repository.hasPermission() - } - - func requestLocationPermission() { - - self.repository.requestLocationPermission() - } - - func checkLocationAuthStatus() -> AuthStatus { - - return self.repository.checkLocationAuthStatus() - } -} diff --git a/SOOUM/SOOUM/Domain/UseCases/TagUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/TagUseCaseImpl.swift deleted file mode 100644 index ae9f4e60..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/TagUseCaseImpl.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// TagUseCaseImpl.swift -// SOOUM -// -// Created by 오현식 on 10/8/25. -// - -import Foundation - -import RxSwift - -class TagUseCaseImpl: TagUseCase { - - private let repository: TagRepository - - init(repository: TagRepository) { - self.repository = repository - } - - func related(keyword: String, size: Int) -> Observable<[TagInfo]> { - - return self.repository.related(keyword: keyword, size: size).map { $0.tagInfos } - } - - func favorites() -> Observable<[FavoriteTagInfo]> { - - return self.repository.favorites().map(\.tagInfos) - } - - func updateFavorite(tagId: String, isFavorite: Bool) -> Observable { - - return self.repository.updateFavorite(tagId: tagId, isFavorite: isFavorite).map { $0 == 200 } - } - - func ranked() -> Observable<[TagInfo]> { - - return self.repository.ranked().map(\.tagInfos) - } - - func tagCards(tagId: String, lastId: String?) -> Observable<(cardInfos: [ProfileCardInfo], isFavorite: Bool)> { - - return self.repository.tagCards(tagId: tagId, lastId: lastId).map { ($0.cardInfos, $0.isFavorite) } - } -} diff --git a/SOOUM/SOOUM/Domain/UseCases/TransferAccountUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/TransferAccountUseCaseImpl.swift new file mode 100644 index 00000000..843f4136 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/TransferAccountUseCaseImpl.swift @@ -0,0 +1,36 @@ +// +// TransferAccountUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class TransferAccountUseCaseImpl: TransferAccountUseCase { + + private let repository: SettingsRepository + + init(repository: SettingsRepository) { + self.repository = repository + } + + func issue() -> Observable { + + return self.repository.issue().map(\.transferInfo) + } + + func update() -> Observable { + + return self.repository.update().map(\.transferInfo) + } + + func enter(code: String, encryptedDeviceId: String) -> Observable { + + return self.repository.enter( + code: code, + encryptedDeviceId: encryptedDeviceId + ) + .map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UpdateCardLikeUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UpdateCardLikeUseCaseImpl.swift new file mode 100644 index 00000000..9dd8734e --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/UpdateCardLikeUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// UpdateCardLikeUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class UpdateCardLikeUseCaseImpl: UpdateCardLikeUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func updateLike(cardId: String, isLike: Bool) -> Observable { + + return self.repository.updateLike(id: cardId, isLike: isLike).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UpdateFollowUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UpdateFollowUseCaseImpl.swift new file mode 100644 index 00000000..0b7f5c6a --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/UpdateFollowUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// UpdateFollowUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class UpdateFollowUseCaseImpl: UpdateFollowUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func updateFollowing(userId: String, isFollow: Bool) -> Observable { + + return self.repository.updateFollowing(userId: userId, isFollow: isFollow).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UpdateNotifyUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UpdateNotifyUseCaseImpl.swift new file mode 100644 index 00000000..ff497334 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/UpdateNotifyUseCaseImpl.swift @@ -0,0 +1,32 @@ +// +// UpdateNotifyUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class UpdateNotifyUseCaseImpl: UpdateNotifyUseCase { + + private let repository: SettingsRepository + + init(repository: SettingsRepository) { + self.repository = repository + } + + func notificationStatus() -> Bool { + + return self.repository.notificationStatus() + } + + func switchNotification(on: Bool) -> Observable { + + return self.repository.switchNotification(on: on).map { _ in } + } + + func updateNotify(isAllowNotify: Bool) -> Observable { + + return self.repository.updateNotify(isAllowNotify: isAllowNotify).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UpdateTagFavoriteUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UpdateTagFavoriteUseCaseImpl.swift new file mode 100644 index 00000000..6bc74cf4 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/UpdateTagFavoriteUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// UpdateTagFavoriteUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class UpdateTagFavoriteUseCaseImpl: UpdateTagFavoriteUseCase { + + private let repository: TagRepository + + init(repository: TagRepository) { + self.repository = repository + } + + func updateFavorite(tagId: String, isFavorite: Bool) -> Observable { + + return self.repository.updateFavorite(tagId: tagId, isFavorite: isFavorite).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UpdateUserInfoUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UpdateUserInfoUseCaseImpl.swift new file mode 100644 index 00000000..b658facb --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/UpdateUserInfoUseCaseImpl.swift @@ -0,0 +1,26 @@ +// +// UpdateUserInfoUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class UpdateUserInfoUseCaseImpl: UpdateUserInfoUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func updateUserInfo(nickname: String?, imageName: String?) -> Observable { + + return self.repository.updateMyProfile( + nickname: nickname, + imageName: imageName + ) + .map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UploadUserImageUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UploadUserImageUseCaseImpl.swift new file mode 100644 index 00000000..20fe2cf3 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/UploadUserImageUseCaseImpl.swift @@ -0,0 +1,32 @@ +// +// UploadUserImageUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class UploadUserImageUseCaseImpl: UploadUserImageUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func presignedURL() -> Observable { + + return self.repository.presignedURL().map(\.imageUrlInfo) + } + + func uploadToS3(_ data: Data, with url: URL) -> Observable { + + return self.repository.uploadImage(data, with: url).map { (try? $0.get()) == 200 } + } + + func registerImageName(imageName: String) -> Observable { + + return self.repository.updateImage(imageName: imageName).map { $0 == 200 } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/UserUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/UserUseCaseImpl.swift deleted file mode 100644 index ef559065..00000000 --- a/SOOUM/SOOUM/Domain/UseCases/UserUseCaseImpl.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// UserUseCaseImpl.swift -// SOOUM -// -// Created by 오현식 on 9/17/25. -// - -import Foundation - -import RxSwift - -class UserUseCaseImpl: UserUseCase { - - private let repository: UserRepository - - init(repository: UserRepository) { - self.repository = repository - } - - func isAvailableCheck() -> Observable { - - return self.repository.checkAvailable().map { $0.checkAvailable } - } - - func nickname() -> Observable { - - return self.repository.nickname().map { $0.nickname } - } - - func isNicknameValid(nickname: String) -> Observable { - - return self.repository.validateNickname(nickname: nickname).map { $0.isAvailable } - } - - func updateNickname(nickname: String) -> Observable { - - return self.repository.updateNickname(nickname: nickname).map { $0 == 200 } - } - - func presignedURL() -> Observable { - - return self.repository.presignedURL().map { $0.imageUrlInfo } - } - - func uploadImage(_ data: Data, with url: URL) -> Observable { - - return self.repository.uploadImage(data, with: url) - .map { (try? $0.get()) == 200 } - } - - func updateImage(imageName: String) -> Observable { - - return self.repository.updateImage(imageName: imageName).map { $0 == 200 } - } - - func updateFCMToken(fcmToken: String) -> Observable { - - return self.repository.updateFCMToken(fcmToken: fcmToken).map { $0 == 200 } - } - - func postingPermission() -> Observable { - - return self.repository.postingPermission().map { $0.postingPermission } - } - - func profile(userId: String?) -> Observable { - - return self.repository.profile(userId: userId).map { $0.profileInfo } - } - - func updateMyProfile(nickname: String?, imageName: String?) -> Observable { - - return self.repository.updateMyProfile(nickname: nickname, imageName: imageName).map { $0 == 200 } - } - - func feedCards(userId: String, lastId: String?) -> Observable<[ProfileCardInfo]> { - - return self.repository.feedCards(userId: userId, lastId: lastId).map { $0.cardInfos } - } - - func myCommentCards(lastId: String?) -> Observable<[ProfileCardInfo]> { - - return self.repository.myCommentCards(lastId: lastId).map { $0.cardInfos } - } - - func followers(userId: String, lastId: String?) -> Observable<[FollowInfo]> { - - return self.repository.followers(userId: userId, lastId: lastId).map { $0.followInfos } - } - - func followings(userId: String, lastId: String?) -> Observable<[FollowInfo]> { - - return self.repository.followings(userId: userId, lastId: lastId).map { $0.followInfos } - } - - func updateFollowing(userId: String, isFollow: Bool) -> Observable { - - return self.repository.updateFollowing(userId: userId, isFollow: isFollow).map { $0 == 200 } - } - - func updateBlocked(id: String, isBlocked: Bool) -> Observable { - - return self.repository.updateBlocked(id: id, isBlocked: isBlocked).map { $0 == 200 } - } - - func updateNotify(isAllowNotify: Bool) -> Observable { - - return self.repository.updateNotify(isAllowNotify: isAllowNotify).map { $0 == 200 } - } -} diff --git a/SOOUM/SOOUM/Domain/UseCases/ValidateNicknameUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/ValidateNicknameUseCaseImpl.swift new file mode 100644 index 00000000..884e889d --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/ValidateNicknameUseCaseImpl.swift @@ -0,0 +1,27 @@ +// +// ValidateNicknameUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class ValidateNicknameUseCaseImpl: ValidateNicknameUseCase { + + private let repository: UserRepository + + init(repository: UserRepository) { + self.repository = repository + } + + func nickname() -> Observable { + + return self.repository.nickname().map(\.nickname) + } + + func checkValidation(nickname: String) -> Observable { + + return self.repository.validateNickname(nickname: nickname).map(\.isAvailable) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/ValidateUserUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/ValidateUserUseCaseImpl.swift new file mode 100644 index 00000000..605b52e8 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/ValidateUserUseCaseImpl.swift @@ -0,0 +1,34 @@ +// +// ValidateUserUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class ValidateUserUseCaseImpl: ValidateUserUseCase { + + private let userRepository: UserRepository + private let settingsRepository: SettingsRepository + + init(user: UserRepository, settings: SettingsRepository) { + self.userRepository = user + self.settingsRepository = settings + } + + func checkValidation() -> Observable { + + return self.userRepository.checkAvailable().map(\.checkAvailable) + } + + func iswithdrawn() -> Observable { + + return self.settingsRepository.rejoinableDate().map(\.rejoinableDate) + } + + func postingPermission() -> Observable { + + return self.userRepository.postingPermission().map(\.postingPermission) + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/WriteCardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/WriteCardUseCaseImpl.swift new file mode 100644 index 00000000..759b127f --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/WriteCardUseCaseImpl.swift @@ -0,0 +1,69 @@ +// +// WriteCardUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 12/3/25. +// + +import RxSwift + +final class WriteCardUseCaseImpl: WriteCardUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func writeFeed( + isDistanceShared: Bool, + latitude: String?, + longitude: String?, + content: String, + font: String, + imgType: String, + imgName: String, + isStory: Bool, + tags: [String] + ) -> Observable { + + return self.repository.writeCard( + isDistanceShared: isDistanceShared, + latitude: latitude, + longitude: longitude, + content: content, + font: font, + imgType: imgType, + imgName: imgName, + isStory: isStory, + tags: tags + ) + .map(\.cardId) + } + + func writeComment( + parentCardId: String, + isDistanceShared: Bool, + latitude: String?, + longitude: String?, + content: String, + font: String, + imgType: String, + imgName: String, + tags: [String] + ) -> Observable { + + return self.repository.writeComment( + id: parentCardId, + isDistanceShared: isDistanceShared, + latitude: latitude, + longitude: longitude, + content: content, + font: font, + imgType: imgType, + imgName: imgName, + tags: tags + ) + .map(\.cardId) + } +} diff --git a/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift b/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift index 00fcafa2..15aee465 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift @@ -16,8 +16,7 @@ extension Notification.Name { static let changedLocationAuthorization = Notification.Name("changedLocationAuthorization") /// Should scroll to top static let scollingToTopWithAnimation = Notification.Name("scollingToTopWithAnimation") - /// Should reload - static let reloadData = Notification.Name("reloadData") + /// Should reload detail static let reloadDetailData = Notification.Name("reloadDetailData") /// Updated report state static let updatedReportState = Notification.Name("updatedReportState") diff --git a/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift b/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift index 80664267..8517be15 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift @@ -8,7 +8,7 @@ import UIKit extension UIRefreshControl { - + /// RefreshControl 에 offset 설정 func beginRefreshingWithOffset(_ offset: CGFloat) { self.bounds.origin.y = -offset self.beginRefreshing() diff --git a/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift b/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift index 38ffc0ab..31c3b716 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/UITextField.swift @@ -29,13 +29,14 @@ extension UITextField { if aleadyFull { // 텍스트 입력 전에 제한을 벗어남 if isTyped { - // 입력 시 더 이상 입력되지 않음 + // 영어 입력 시 더 이상 입력되지 않음 + guard string.isEnglish == false else { return false } let lastCharacter = String(text[text.index(before: text.endIndex)]) let separatedCharacters = lastCharacter.decomposedStringWithCanonicalMapping.unicodeScalars.map { String($0) } let separatedCharactersCount = separatedCharacters.count // 마지막 문자를 자음 + 모음으로 나누어 갯수에 따라 판단, // 갯수가 1일 때, 모음이면 입력 가능 - if separatedCharactersCount == 1 && string.isConsonant == false { return true } + if separatedCharactersCount == 1 && lastCharacter.isConsonant && string.isConsonant == false { return true } // 갯수가 2일 때, 자음이면 입력 가능 if separatedCharactersCount == 2 && string.isConsonant { return true } // TODO: 겹받침일 때는 고려 X @@ -53,7 +54,7 @@ extension UITextField { self?.selectedTextRange = self?.textRange(from: position, to: position) } } - self.sendActions(for: .valueChanged) + self.sendActions(for: .editingChanged) } } else { // 텍스트 입력 후에 제한을 벗어남 @@ -67,7 +68,7 @@ extension UITextField { self?.selectedTextRange = self?.textRange(from: position, to: position) } } - self.sendActions(for: .valueChanged) + self.sendActions(for: .editingChanged) } return false } else { diff --git a/SOOUM/SOOUM/Extensions/Foundation/Date.swift b/SOOUM/SOOUM/Extensions/Foundation/Date.swift index e24cefd1..b4083af5 100644 --- a/SOOUM/SOOUM/Extensions/Foundation/Date.swift +++ b/SOOUM/SOOUM/Extensions/Foundation/Date.swift @@ -123,7 +123,7 @@ extension Date { } var banEndFormatted: String { - return self.toString("yyyy년 MM월 dd일") + return self.addingTimeInterval(24 * 60 * 60).toString("yyyy년 MM월 dd일") } var banEndDetailFormatted: String { diff --git a/SOOUM/SOOUM/Extensions/Foundation/String.swift b/SOOUM/SOOUM/Extensions/Foundation/String.swift index 091b572e..8c583fcb 100644 --- a/SOOUM/SOOUM/Extensions/Foundation/String.swift +++ b/SOOUM/SOOUM/Extensions/Foundation/String.swift @@ -17,4 +17,12 @@ extension String { let consonantScalarRange: ClosedRange = 12593...12622 return consonantScalarRange ~= scalar } + /// 영어인지 여부 확인 + var isEnglish: Bool { + guard self.isEmpty == false else { return false } + + let pattern = "^[a-zA-Z]+$" + let predicate = NSPredicate(format: "SELF MATCHES %@", pattern) + return predicate.evaluate(with: self) + } } diff --git a/SOOUM/SOOUM/Extensions/Foundation/UserDefaults.swift b/SOOUM/SOOUM/Extensions/Foundation/UserDefaults.swift index caa69282..88f2ba50 100644 --- a/SOOUM/SOOUM/Extensions/Foundation/UserDefaults.swift +++ b/SOOUM/SOOUM/Extensions/Foundation/UserDefaults.swift @@ -13,6 +13,7 @@ extension UserDefaults { enum Keys { static let hasBeenLaunchedBefore: String = "hasBeenLaunchedBefore" static let hasBeenShowMessageGuide: String = "hasBeenShowMessageGuide" + static let hasBeenShowWriteCardGuide: String = "hasBeenShowWriteCardGuide" static let userNickname: String = "userNickname" } @@ -38,6 +39,17 @@ extension UserDefaults { return isFirstLaunch } + // 카드추가 시 가이드 뷰를 위한 flag + static var showGuideView: Bool { + + let isFirstLaunch = !UserDefaults.standard.bool(forKey: Keys.hasBeenShowWriteCardGuide) + if isFirstLaunch { + UserDefaults.standard.set(true, forKey: Keys.hasBeenShowWriteCardGuide) + } + + return isFirstLaunch + } + // Nickname의 전역 사용을 위한 확장 var nickname: String? { get { diff --git a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift index 95070848..caa03d24 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift @@ -70,27 +70,28 @@ class LaunchScreenViewController: BaseNavigationViewController, View { reactor.state.map(\.mustUpdate) .distinctUntilChanged() .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(onNext: { _ in let updateAction = SOMDialogAction( title: Text.updateActionTitle, style: .primary, action: { - #if DEVELOP - // 개발 버전일 때 testFlight로 전환 - let strUrl = "\(Text.testFlightStrUrl)/\(Info.appId)" - if let testFlightUrl = URL(string: strUrl) { - UIApplication.shared.open(testFlightUrl, options: [:], completionHandler: nil) + UIApplication.topViewController?.dismiss(animated: true) { + #if DEVELOP + // 개발 버전일 때 testFlight로 전환 + let strUrl = "\(Text.testFlightStrUrl)/\(Info.appId)" + if let testFlightUrl = URL(string: strUrl) { + UIApplication.shared.open(testFlightUrl, options: [:], completionHandler: nil) + } + #elseif PRODUCTION + // 운영 버전일 때 app store로 전환 + let strUrl = "\(Text.appStoreStrUrl)\(Info.appId)" + if let appStoreUrl = URL(string: strUrl) { + UIApplication.shared.open(appStoreUrl, options: [:], completionHandler: nil) + } + #endif } - #elseif PRODUCTION - // 운영 버전일 때 app store로 전환 - let strUrl = "\(Text.appStoreStrUrl)\(Info.appId)" - if let appStoreUrl = URL(string: strUrl) { - UIApplication.shared.open(appStoreUrl, options: [:], completionHandler: nil) - } - #endif - - UIApplication.topViewController?.dismiss(animated: true) } ) @@ -107,6 +108,7 @@ class LaunchScreenViewController: BaseNavigationViewController, View { let isRegistered = reactor.state.map(\.isRegistered).distinctUntilChanged().share() isRegistered .filter { $0 == true } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let viewController = MainTabBarController() viewController.reactor = reactor.reactorForMainTabBar() @@ -119,6 +121,7 @@ class LaunchScreenViewController: BaseNavigationViewController, View { // 로그인 실패 시 온보딩 화면으로 전환 isRegistered .filter { $0 == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let viewController = OnboardingViewController() viewController.reactor = reactor.reactorForOnboarding() diff --git a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift index bccfbaad..46ee10c4 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift @@ -33,14 +33,12 @@ class LaunchScreenViewReactor: Reactor { enum Mutation { case check(Bool) case updateIsRegistered(Bool) - case appFlag(Bool) } struct State { fileprivate(set) var mustUpdate: Bool /// deviceId 서버 등록 여부, 로그인 성공 여부 fileprivate(set) var isRegistered: Bool? - fileprivate(set) var appFlag: Bool? } var initialState: State = .init( @@ -82,12 +80,8 @@ class LaunchScreenViewReactor: Reactor { switch mutation { case let .check(mustUpdate): newState.mustUpdate = mustUpdate - case let .updateIsRegistered(isRegistered): newState.isRegistered = isRegistered - - case let .appFlag(appFlag): - newState.appFlag = appFlag } return newState } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift index d059a2f5..abe3efe0 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift @@ -95,7 +95,7 @@ class OnboardingCompletedViewController: BaseNavigationViewController, View { func bind(reactor: OnboardingCompletedViewReactor) { // Action - self.confirmButton.rx.throttleTap + self.confirmButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in let viewController = MainTabBarController() viewController.reactor = reactor.reactorForMainTabBar() diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift index a768840d..7d91eace 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift @@ -97,6 +97,11 @@ class OnboardingNicknameSettingViewController: BaseNavigationViewController, Vie func bind(reactor: OnboardingNicknameSettingViewReactor) { // Action + self.rx.viewDidLoad + .map { _ in Reactor.Action.landing } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + let nickname = self.nicknameTextField.textField.rx.text.orEmpty.distinctUntilChanged().share() nickname .debounce(.milliseconds(500), scheduler: MainScheduler.instance) @@ -104,12 +109,10 @@ class OnboardingNicknameSettingViewController: BaseNavigationViewController, Vie .bind(to: reactor.action) .disposed(by: self.disposeBag) - self.rx.viewDidLoad - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - self.nextButton.rx.tap + self.nextButton.rx.throttleTap + .withLatestFrom(reactor.state.map(\.isProcessing)) + .filter { $0 == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let profileImageSettingVC = OnboardingProfileImageSettingViewController() profileImageSettingVC.reactor = reactor.reactorForProfileImage() @@ -120,6 +123,7 @@ class OnboardingNicknameSettingViewController: BaseNavigationViewController, Vie // State reactor.state.map(\.nickname) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, randomText in object.nicknameTextField.text = randomText object.nicknameTextField.textField.sendActions(for: .editingChanged) @@ -128,11 +132,13 @@ class OnboardingNicknameSettingViewController: BaseNavigationViewController, Vie reactor.state.map(\.isValid) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .bind(to: self.nextButton.rx.isEnabled) .disposed(by: self.disposeBag) reactor.state.map(\.errorMessage) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, errorMessage in object.nicknameTextField.guideMessage = errorMessage == nil ? Text.guideMessage : errorMessage object.nicknameTextField.hasError = errorMessage != nil diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewReactor.swift index afdb6282..fc29d6ab 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewReactor.swift @@ -61,34 +61,37 @@ class OnboardingNicknameSettingViewReactor: Reactor { enum Mutation { case updateNickname(String) case updateIsValid(Bool) + case updateIsProcessing(Bool) case updateIsErrorMessage(String?) } struct State { fileprivate(set) var nickname: String fileprivate(set) var isValid: Bool + fileprivate(set) var isProcessing: Bool fileprivate(set) var errorMessage: String? } var initialState: State = .init( nickname: "\(Text.adjectives.randomElement()!) \(Text.nouns.randomElement()!)", isValid: false, + isProcessing: false, errorMessage: nil ) private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase + private let validateNicknameUseCase: ValidateNicknameUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.validateNicknameUseCase = dependencies.rootContainer.resolve(ValidateNicknameUseCase.self) } func mutate(action: Action) -> Observable { switch action { case .landing: - return self.userUseCase.nickname() + return self.validateNicknameUseCase.nickname() .map(Mutation.updateNickname) case let .checkValidate(nickname): @@ -100,8 +103,9 @@ class OnboardingNicknameSettingViewReactor: Reactor { } return .concat([ + .just(.updateIsProcessing(true)), .just(.updateIsErrorMessage(nil)), - self.userUseCase.isNicknameValid(nickname: nickname) + self.validateNicknameUseCase.checkValidation(nickname: nickname) .withUnretained(self) .flatMapLatest { object, isValid -> Observable in @@ -112,7 +116,8 @@ class OnboardingNicknameSettingViewReactor: Reactor { .just(.updateNickname(nickname)), .just(.updateIsErrorMessage(errorMessage)) ]) - } + }, + .just(.updateIsProcessing(false)) ]) } } @@ -124,6 +129,8 @@ class OnboardingNicknameSettingViewReactor: Reactor { newState.nickname = nickname case let .updateIsValid(isValid): newState.isValid = isValid + case let .updateIsProcessing(isProcessing): + newState.isProcessing = isProcessing case let .updateIsErrorMessage(errorMessage): newState.errorMessage = errorMessage } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewController.swift index e4c17e89..356c7698 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewController.swift @@ -162,47 +162,46 @@ class OnboardingViewController: BaseNavigationViewController, View { func bind(reactor: OnboardingViewReactor) { - // Action - self.rx.viewDidLoad - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let startButtonTapped = self.startButton.rx.tap.share() + let startButtonTapped = self.startButton.rx.throttleTap.share() let checkAvailable = reactor.state.map(\.checkAvailable).filterNil().share() + // `숨 시작하기` 버튼을 탭한 이후 최신 상태 반영 + let latestAvailableDidTapped = startButtonTapped + .flatMapLatest { _ in checkAvailable.skip(1).take(1) } + // .observe(on: MainScheduler.instance) + .share() - startButtonTapped - .withLatestFrom(checkAvailable) + // 차단 및 탈퇴한 계정이 아닐 경우 온보딩 화면 전환 + latestAvailableDidTapped .filter { $0.banned == false && $0.withdrawn == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let termsOfServiceViewController = OnboardingTermsOfServiceViewController() termsOfServiceViewController.reactor = reactor.reactorForTermsOfService() object.navigationPush(termsOfServiceViewController, animated: true) } .disposed(by: disposeBag) - - startButtonTapped - .withLatestFrom(checkAvailable) + // 차단된 계정인 경우 팝업 표시 + latestAvailableDidTapped .filter { $0.banned && $0.rejoinAvailableAt != nil } - .subscribe(with: self) { object, checkAvailable in + .compactMap(\.rejoinAvailableAt) + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, rejoinAvailableAt in - if let rejoinAvailableAt = checkAvailable.rejoinAvailableAt { - object.showBannedUserDialog(at: rejoinAvailableAt) - } + object.showBannedUserDialog(at: rejoinAvailableAt) } .disposed(by: self.disposeBag) - startButtonTapped - .withLatestFrom(checkAvailable) + // 탈퇴한 계정인 경우 팝업 표시 + latestAvailableDidTapped .filter { $0.withdrawn && $0.rejoinAvailableAt != nil } - .subscribe(with: self) { object, checkAvailable in + .compactMap(\.rejoinAvailableAt) + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, rejoinAvailableAt in - if let rejoinAvailableAt = checkAvailable.rejoinAvailableAt { - object.showResignUserDialog(at: rejoinAvailableAt) - } + object.showResignUserDialog(at: rejoinAvailableAt) } .disposed(by: self.disposeBag) - - self.oldUserButton.rx.tap + // 계정 이관 화면 이동 + self.oldUserButton.rx.throttleTap .subscribe(with: self) { object, _ in let enterMemberTransferViewController = EnterMemberTransferViewController() enterMemberTransferViewController.reactor = reactor.reactorForEnterTransfer() @@ -210,29 +209,41 @@ class OnboardingViewController: BaseNavigationViewController, View { } .disposed(by: disposeBag) + // Action + self.rx.viewDidLoad + .map { _ in Reactor.Action.landing } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + startButtonTapped + .map { _ in Reactor.Action.check } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + // State checkAvailable .take(1) .filter { $0.banned && $0.rejoinAvailableAt != nil } - .subscribe(with: self) { object, checkAvailable in + .compactMap(\.rejoinAvailableAt) + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, rejoinAvailableAt in - if let rejoinAvailableAt = checkAvailable.rejoinAvailableAt { - object.showBannedUserDialog(at: rejoinAvailableAt) - } + object.showBannedUserDialog(at: rejoinAvailableAt) } .disposed(by: self.disposeBag) checkAvailable .take(1) .filter { $0.withdrawn && $0.rejoinAvailableAt != nil } - .subscribe(with: self) { object, checkAvailable in + .compactMap(\.rejoinAvailableAt) + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, rejoinAvailableAt in - if let rejoinAvailableAt = checkAvailable.rejoinAvailableAt { - object.showResignUserDialog(at: rejoinAvailableAt) - } + object.showResignUserDialog(at: rejoinAvailableAt) } .disposed(by: self.disposeBag) reactor.state.map(\.shouldHideTransfer) + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, shouldHide in object.oldUserButton.isHidden = shouldHide } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewReactor.swift index 264eaab3..d6039e5d 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Onboarding/OnboardingViewReactor.swift @@ -7,11 +7,11 @@ import ReactorKit - class OnboardingViewReactor: Reactor { enum Action: Equatable { case landing + case check } @@ -30,13 +30,13 @@ class OnboardingViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase - private let settingsUseCase: SettingsUseCase + private let validateUserUseCase: ValidateUserUseCase + private let updateNotifyUseCase: UpdateNotifyUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.validateUserUseCase = dependencies.rootContainer.resolve(ValidateUserUseCase.self) + self.updateNotifyUseCase = dependencies.rootContainer.resolve(UpdateNotifyUseCase.self) } func mutate(action: Action) -> Observable { @@ -44,10 +44,15 @@ class OnboardingViewReactor: Reactor { case .landing: return .concat([ - self.check(), - self.settingsUseCase.switchNotification(on: true) + self.validateUserUseCase.checkValidation() + .map(Mutation.check), + self.updateNotifyUseCase.switchNotification(on: true) .flatMapLatest { _ -> Observable in .empty() } ]) + case .check: + + return self.validateUserUseCase.checkValidation() + .map(Mutation.check) } } @@ -61,15 +66,6 @@ class OnboardingViewReactor: Reactor { } } -extension OnboardingViewReactor { - - private func check() -> Observable { - - return self.userUseCase.isAvailableCheck() - .map(Mutation.check) - } -} - extension OnboardingViewReactor { func reactorForTermsOfService() -> OnboardingTermsOfServiceViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift index 826c02f3..bc00f594 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift @@ -157,7 +157,7 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, // Action Observable.merge( self.profileImageView.rx.tapGesture().when(.ended).map { _ in }, - self.cameraButton.rx.tap.asObservable() + self.cameraButton.rx.throttleTap.asObservable() ) .subscribe(with: self) { object, _ in @@ -176,12 +176,12 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, } .disposed(by: self.disposeBag) - self.completeButton.rx.tap + self.completeButton.rx.throttleTap(.seconds(3)) .map { _ in Reactor.Action.signUp } .bind(to: reactor.action) .disposed(by: self.disposeBag) - self.passButton.rx.tap + self.passButton.rx.throttleTap(.seconds(3)) .map { _ in Reactor.Action.signUp } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -190,6 +190,7 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, reactor.state.map(\.isSignUp) .distinctUntilChanged() .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let viewController = OnboardingCompletedViewController() viewController.reactor = reactor.reactorForCompleted() @@ -199,6 +200,7 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, reactor.state.map(\.isLoading) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self.loadingIndicatorView) { loadingIndicatorView, isLoading in if isLoading { loadingIndicatorView.startAnimating() @@ -211,31 +213,16 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, reactor.state.map(\.hasErrors) .distinctUntilChanged() .filter { $0 } - .subscribe(onNext: { _ in + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, _ in - let actions: [SOMDialogAction] = [ - .init( - title: Text.inappositeDialogConfirmButtonTitle, - style: .primary, - action: { - UIApplication.topViewController?.dismiss(animated: true) { - reactor.action.onNext(.setDefaultImage) - } - } - ) - ] - - SOMDialogViewController.show( - title: Text.inappositeDialogTitle, - message: Text.inappositeDialogMessage, - textAlignment: .left, - actions: actions - ) - }) + object.showInappositeDialog(reactor) + } .disposed(by: self.disposeBag) reactor.state.map(\.profileImage) .distinctUntilChanged() + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, profileImage in object.profileImageView.image = profileImage ?? .init(.image(.v2(.profile_large))) @@ -293,14 +280,15 @@ extension OnboardingProfileImageSettingViewController { title: Text.settingActionTitle, style: .primary, action: { - let application = UIApplication.shared - let openSettingsURLString: String = UIApplication.openSettingsURLString - if let settingsURL = URL(string: openSettingsURLString), - application.canOpenURL(settingsURL) { - application.open(settingsURL) + UIApplication.topViewController?.dismiss(animated: true) { + + let application = UIApplication.shared + let openSettingsURLString: String = UIApplication.openSettingsURLString + if let settingsURL = URL(string: openSettingsURLString), + application.canOpenURL(settingsURL) { + application.open(settingsURL) + } } - - UIApplication.topViewController?.dismiss(animated: true) } ) @@ -311,6 +299,28 @@ extension OnboardingProfileImageSettingViewController { ) } + func showInappositeDialog(_ reactor: OnboardingProfileImageSettingViewReactor) { + + let actions: [SOMDialogAction] = [ + .init( + title: Text.inappositeDialogConfirmButtonTitle, + style: .primary, + action: { + UIApplication.topViewController?.dismiss(animated: true) { + reactor.action.onNext(.setDefaultImage) + } + } + ) + ] + + SOMDialogViewController.show( + title: Text.inappositeDialogTitle, + message: Text.inappositeDialogMessage, + textAlignment: .left, + actions: actions + ) + } + func showPicker(for screen: YPPickerScreen) { var config = YPImagePickerConfiguration() @@ -349,8 +359,8 @@ extension OnboardingProfileImageSettingViewController { picker?.dismiss(animated: true, completion: nil) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self.present(picker, animated: true, completion: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in + self?.present(picker, animated: true, completion: nil) } } } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift index 0e2624b6..4bd0d3d1 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift @@ -42,15 +42,15 @@ class OnboardingProfileImageSettingViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase private let authUseCase: AuthUseCase + private let uploadUserImageUseCase: UploadUserImageUseCase private let nickname: String init(dependencies: AppDIContainerable, nickname: String) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) self.authUseCase = dependencies.rootContainer.resolve(AuthUseCase.self) + self.uploadUserImageUseCase = dependencies.rootContainer.resolve(UploadUserImageUseCase.self) self.nickname = nickname } @@ -114,7 +114,7 @@ extension OnboardingProfileImageSettingViewReactor { if let imageData = image.jpegData(compressionQuality: 0.5), let url = URL(string: presignedInfo.imgUrl) { - return object.userUseCase.uploadImage(imageData, with: url) + return object.uploadUserImageUseCase.uploadToS3(imageData, with: url) .flatMapLatest { isSuccess -> Observable in let image = isSuccess ? image : nil @@ -131,7 +131,7 @@ extension OnboardingProfileImageSettingViewReactor { private func presignedURL() -> Observable { - return self.userUseCase.presignedURL() + return self.uploadUserImageUseCase.presignedURL() } private var catchClosure: ((Error) throws -> Observable ) { diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/OnboardingTermsOfServiceViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/OnboardingTermsOfServiceViewController.swift index 631b7721..5c2f6b52 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/OnboardingTermsOfServiceViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/OnboardingTermsOfServiceViewController.swift @@ -23,9 +23,9 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View static let guideMessageTitle: String = "숨 서비스 이용을 위해\n동의해주세요" - static let termsOfSeviceUrlString: String = "https://mewing-space-6d3.notion.site/3f92380d536a4b569921d2809ed147ef?pvs=4" - static let locationServiceUrlString: String = "https://mewing-space-6d3.notion.site/45d151f68ba74b23b24483ad8b2662b4?pvs=4" - static let privacyPolicyUrlString: String = "https://mewing-space-6d3.notion.site/44e378c9d11d45159859492434b6b128?pvs=4" + static let termsOfSeviceUrlString: String = "https://adjoining-guanaco-d0a.notion.site/26b2142ccaa38076b491df099cd7b559" + static let locationServiceUrlString: String = "https://adjoining-guanaco-d0a.notion.site/26b2142ccaa380f1bfafe99f5f8a10f1?pvs=74" + static let privacyPolicyUrlString: String = "https://adjoining-guanaco-d0a.notion.site/26b2142ccaa38059a1dbf3e6b6b6b4e6?pvs=74" static let nextButtonTitle: String = "다음" } @@ -162,6 +162,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View .disposed(by: self.disposeBag) self.termsOfServiceCellView.rx.moveSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(onNext: { _ in if UIApplication.shared.canOpenURL(TermsOfService.termsOfService.url) { UIApplication.shared.open( @@ -179,6 +180,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View .disposed(by: self.disposeBag) self.locationServiceCellView.rx.moveSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(onNext: { _ in if UIApplication.shared.canOpenURL(TermsOfService.locationService.url) { UIApplication.shared.open( @@ -196,6 +198,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View .disposed(by: self.disposeBag) self.privacyPolicyCellView.rx.moveSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(onNext: { _ in if UIApplication.shared.canOpenURL(TermsOfService.privacyPolicy.url) { UIApplication.shared.open( @@ -207,7 +210,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View }) .disposed(by: self.disposeBag) - self.nextButton.rx.tap + self.nextButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in let nicknameSettingVC = OnboardingNicknameSettingViewController() nicknameSettingVC.reactor = reactor.reactorForNickname() @@ -218,6 +221,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View // State reactor.state.map(\.isAllAgreed) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, isAllAgreed in object.agreeAllButtonView.updateState(isAllAgreed, animated: false) object.nextButton.isEnabled = isAllAgreed @@ -226,6 +230,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View reactor.state.map(\.isTermsOfServiceAgreed) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, isTermsOfServiceAgreed in object.termsOfServiceCellView.updateState(isTermsOfServiceAgreed, animated: false) } @@ -233,6 +238,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View reactor.state.map(\.isLocationAgreed) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, isLocationAgreed in object.locationServiceCellView.updateState(isLocationAgreed, animated: false) } @@ -240,6 +246,7 @@ class OnboardingTermsOfServiceViewController: BaseNavigationViewController, View reactor.state.map(\.isPrivacyPolicyAgreed) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, isPrivacyPolicyAgreed in object.privacyPolicyCellView.updateState(isPrivacyPolicyAgreed, animated: false) } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceAgreeButtonView.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceAgreeButtonView.swift index 40c70988..13e9790f 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceAgreeButtonView.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceAgreeButtonView.swift @@ -89,8 +89,8 @@ class TermsOfServiceAgreeButtonView: UIView { let animationDuration: TimeInterval = animated ? 0.25 : 0 - UIView.animate(withDuration: animationDuration) { - self.checkImageView.tintColor = state ? .som.v2.pDark : .som.v2.gray400 + UIView.animate(withDuration: animationDuration) { [weak self] in + self?.checkImageView.tintColor = state ? .som.v2.pDark : .som.v2.gray400 } } } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceCellView.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceCellView.swift index 80fc4849..6bf867cc 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceCellView.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/TermsOfService/Views/TermsOfServiceCellView.swift @@ -102,8 +102,8 @@ class TermsOfServiceCellView: UIView { let animationDuration: TimeInterval = animated ? 0.25 : 0 - UIView.animate(withDuration: animationDuration) { - self.checkBoxImageView.tintColor = state ? .som.v2.pDark : .som.v2.gray200 + UIView.animate(withDuration: animationDuration) { [weak self] in + self?.checkBoxImageView.tintColor = state ? .som.v2.pDark : .som.v2.gray200 } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift index e5d5ba14..4515fc02 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift @@ -258,9 +258,9 @@ class DetailViewCell: UICollectionViewCell { self.memberInfoView.distance = model.distance self.memberInfoView.createAt = model.createdAt - if let prevCardImgURL = model.prevCardImgURL { + if let prevCardInfo = model.prevCardInfo { - self.prevCardBackgroundImageView.setImage(strUrl: prevCardImgURL) + self.prevCardBackgroundImageView.setImage(strUrl: prevCardInfo.prevCardImgURL) self.prevCardBackgroundImageView.isHidden = false self.prevCardBackgroundButton.isHidden = false @@ -270,7 +270,7 @@ class DetailViewCell: UICollectionViewCell { self.prevCardBackgroundButton.isHidden = true } - if let isPrevCardDeleted = model.isPrevCardDeleted, isPrevCardDeleted { + if let isPrevCardDeleted = model.prevCardInfo?.isPrevCardDeleted, isPrevCardDeleted { self.prevCardBackgroundImageView.image = nil } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift index f4f80402..f8834cff 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift @@ -105,7 +105,6 @@ class DetailViewController: BaseNavigationViewController, View { // MARK: Variables private var detailCard: DetailCardInfo = .defaultValue - private var commentCards: [BaseCardInfo] = [] private var isDeleted = false @@ -149,15 +148,6 @@ class DetailViewController: BaseNavigationViewController, View { override func setupNaviBar() { super.setupNaviBar() - guard let reactor = self.reactor else { return } - - self.navigationBar.title = reactor.entranceCardType == .feed ? - Text.feedDetailNavigationTitle : - Text.commentDetailNavigationTitle - - if reactor.entranceCardType == .comment { - self.navigationBar.setLeftButtons([self.leftHomeButton]) - } self.navigationBar.setRightButtons([self.rightMoreButton]) } @@ -203,6 +193,7 @@ class DetailViewController: BaseNavigationViewController, View { rightMoreButtonDidTap .withLatestFrom(detailCard) .filter { $0.isOwnCard } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.actions = [ @@ -231,6 +222,7 @@ class DetailViewController: BaseNavigationViewController, View { .withLatestFrom(Observable.combineLatest(detailCard, isBlocked, isReported)) .filter { $0.0.isOwnCard == false } .map { ($0.0.nickname, $0.1, $0.2) } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, combined in let (nickname, isBlocked, isReported) = combined @@ -262,7 +254,7 @@ class DetailViewController: BaseNavigationViewController, View { let reportViewController = ReportViewController() reportViewController.reactor = reactor.reactorForReport() - object?.navigationPush(reportViewController, animated: true, bottomBarHidden: true) + object?.navigationPush(reportViewController, animated: true) } } ) @@ -277,16 +269,16 @@ class DetailViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) // 카드 삭제 후 X 버튼 액션 - self.rightDeleteButton.rx.throttleTap + self.rightDeleteButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in - object.navigationPop(to: HomeViewController.self, animated: false, bottomBarHidden: false) + object.navigationPop(animated: false) } .disposed(by: self.disposeBag) - // 답카드 홈 버튼 액션 - self.leftHomeButton.rx.throttleTap + // 댓글카드 홈 버튼 액션 + self.leftHomeButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in - object.navigationPop(to: HomeViewController.self, animated: false, bottomBarHidden: false) + object.navigationPopToRoot(animated: false) } .disposed(by: self.disposeBag) @@ -315,6 +307,20 @@ class DetailViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) + reactor.state.map(\.isFeed) + .filterNil() + .observe(on: MainScheduler.asyncInstance) + .subscribe(with: self) { object, isFeed in + object.navigationBar.title = isFeed ? + Text.feedDetailNavigationTitle : + Text.commentDetailNavigationTitle + + if isFeed == false { + object.navigationBar.setLeftButtons([object.leftHomeButton]) + } + } + .disposed(by: self.disposeBag) + detailCard .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, detailCard in @@ -343,21 +349,21 @@ class DetailViewController: BaseNavigationViewController, View { let willPushEnabled = reactor.state.map(\.willPushEnabled).distinctUntilChanged().filterNil() willPushEnabled - .filter { $0 } + .filter { $0 == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let writeCardViewController = WriteCardViewController() writeCardViewController.reactor = reactor.reactorForWriteCard() object.navigationPush( writeCardViewController, - animated: true, - bottomBarHidden: true + animated: true ) { _ in reactor.action.onNext(.resetPushState) } } .disposed(by: self.disposeBag) willPushEnabled - .filter { $0 == false } + .filter { $0 } .map { _ in Reactor.Action.resetPushState } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -389,7 +395,7 @@ class DetailViewController: BaseNavigationViewController, View { isBlocked .filter { $0 == false } - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let title = Text.blockToastLeadingTitle + object.detailCard.nickname + Text.blockToastTrailingTitle @@ -413,8 +419,7 @@ class DetailViewController: BaseNavigationViewController, View { .filter { $0 } .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in - NotificationCenter.default.post(name: .reloadData, object: nil, userInfo: nil) - if reactor.entranceCardType == .comment { + if reactor.currentState.isFeed == false { NotificationCenter.default.post(name: .reloadDetailData, object: nil, userInfo: nil) } @@ -495,7 +500,7 @@ extension DetailViewController: UICollectionViewDataSource { type: .other, object.detailCard.memberId ) - object.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + object.navigationPush(profileViewController, animated: true) } } .disposed(by: cell.disposeBag) @@ -508,7 +513,7 @@ extension DetailViewController: UICollectionViewDataSource { with: tagInfo.id, title: tagInfo.text ) - object.navigationPush(tagCollectViewController, animated: true, bottomBarHidden: true) + object.navigationPush(tagCollectViewController, animated: true) } .disposed(by: cell.disposeBag) @@ -529,15 +534,15 @@ extension DetailViewController: UICollectionViewDataSource { /// 현재 쌓인 viewControllers 중 바로 이전 viewController가 전환해야 할 전글이라면 naviPop if let naviStackCount = object.navigationController?.viewControllers.count, let prevViewController = object.navigationController?.viewControllers[naviStackCount - 2] as? DetailViewController, - prevViewController.reactor?.selectedCardId == object.detailCard.prevCardId { + prevViewController.reactor?.selectedCardId == object.detailCard.prevCardInfo?.prevCardId { object.navigationPop() } else { /// 없다면 새로운 viewController로 naviPush - guard let prevCardId = object.detailCard.prevCardId else { return } + guard let prevCardId = object.detailCard.prevCardInfo?.prevCardId else { return } let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForPush(prevCardId) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + object.navigationPush(detailViewController, animated: true) } } .disposed(by: cell.disposeBag) @@ -567,7 +572,7 @@ extension DetailViewController: UICollectionViewDataSource { .subscribe(with: self) { object, selectedId in let viewController = DetailViewController() viewController.reactor = reactor.reactorForPush(selectedId) - object.navigationPush(viewController, animated: true, bottomBarHidden: true) + object.navigationPush(viewController, animated: true) } .disposed(by: footer.disposeBag) @@ -628,7 +633,13 @@ extension DetailViewController: UICollectionViewDelegateFlowLayout { } // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.collectionView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift index 0a6ceda2..32014ac8 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift @@ -10,11 +10,6 @@ import ReactorKit class DetailViewReactor: Reactor { - enum EntranceType { - case push - case navi - } - enum Action: Equatable { case landing case refresh @@ -28,6 +23,7 @@ class DetailViewReactor: Reactor { } enum Mutation { + case cardType(Bool) case detailCard(DetailCardInfo?) case commentCards([BaseCardInfo]) case moreComment([BaseCardInfo]) @@ -41,6 +37,7 @@ class DetailViewReactor: Reactor { } struct State { + fileprivate(set) var isFeed: Bool? fileprivate(set) var detailCard: DetailCardInfo? fileprivate(set) var commentCards: [BaseCardInfo] fileprivate(set) var isRefreshing: Bool @@ -53,6 +50,7 @@ class DetailViewReactor: Reactor { } var initialState: State = .init( + isFeed: nil, detailCard: nil, commentCards: [], isRefreshing: false, @@ -65,27 +63,22 @@ class DetailViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let cardUseCase: CardUseCase - private let userUseCase: UserUseCase - private let settingsUseCase: SettingsUseCase + private let fetchCardDetailUseCase: FetchCardDetailUseCase + private let deleteCardUseCase: DeleteCardUseCase + private let updateCardLikeUseCase: UpdateCardLikeUseCase + private let blockUserUseCase: BlockUserUseCase + private let locationUseCase: LocationUseCase - let entranceCardType: EntranceCardType - let entranceType: EntranceType let selectedCardId: String - init( - dependencies: AppDIContainerable, - _ entranceCardType: EntranceCardType, - type entranceType: EntranceType = .navi, - with selectedCardId: String - ) { + init(dependencies: AppDIContainerable, with selectedCardId: String) { self.dependencies = dependencies - self.cardUseCase = dependencies.rootContainer.resolve(CardUseCase.self) - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.fetchCardDetailUseCase = dependencies.rootContainer.resolve(FetchCardDetailUseCase.self) + self.deleteCardUseCase = dependencies.rootContainer.resolve(DeleteCardUseCase.self) + self.updateCardLikeUseCase = dependencies.rootContainer.resolve(UpdateCardLikeUseCase.self) + self.blockUserUseCase = dependencies.rootContainer.resolve(BlockUserUseCase.self) + self.locationUseCase = dependencies.rootContainer.resolve(LocationUseCase.self) - self.entranceCardType = entranceCardType - self.entranceType = entranceType self.selectedCardId = selectedCardId } @@ -93,9 +86,24 @@ class DetailViewReactor: Reactor { switch action { case .landing: + let coordinate = self.locationUseCase.coordinate() + let latitude = coordinate.latitude + let longitude = coordinate.longitude + return .concat([ - self.detailCard() - .catch(self.catchClosure), + self.fetchCardDetailUseCase.detailCard( + id: self.selectedCardId, + latitude: latitude, + longitude: longitude + ) + .flatMapLatest { detailCardInfo -> Observable in + return .concat([ + .just(.cardType(detailCardInfo.prevCardInfo == nil)), + .just(.updateReported(detailCardInfo.isReported)), + .just(.detailCard(detailCardInfo)) + ]) + } + .catch(self.catchClosure), self.commentCards() ]) case .refresh: @@ -112,13 +120,13 @@ class DetailViewReactor: Reactor { return self.fetchMoreCommentCards(lastId) case .delete: - return self.cardUseCase.deleteCard(id: self.selectedCardId) + return self.deleteCardUseCase.delete(cardId: self.selectedCardId) .map(Mutation.updateIsDeleted) case let .block(isBlocked): guard let memberId = self.currentState.detailCard?.memberId else { return .empty() } - return self.userUseCase.updateBlocked(id: memberId, isBlocked: isBlocked) + return self.blockUserUseCase.updateBlocked(userId: memberId, isBlocked: isBlocked) .flatMapLatest { isBlockedSuccess -> Observable in /// isBlocked == true 일 때, 차단 요청 return isBlockedSuccess ? .just(.updateIsBlocked(isBlocked == false)) : .empty() @@ -128,7 +136,7 @@ class DetailViewReactor: Reactor { return .concat([ .just(.updateIsLiked(false)), - self.cardUseCase.updateLike(id: self.selectedCardId, isLike: isLike) + self.updateCardLikeUseCase.updateLike(cardId: self.selectedCardId, isLike: isLike) .filter { $0 } .withUnretained(self) .flatMapLatest { object, _ -> Observable in @@ -141,14 +149,13 @@ class DetailViewReactor: Reactor { return .just(.updateReported(isReported)) case .willPushToWrite: - return self.detailCard() - .map { _ in .willPushToWrite(true) } - .catch { _ in - return .concat([ - .just(.willPushToWrite(false)), - .just(.updateIsDeleted(true)) - ]) - } + return self.fetchCardDetailUseCase.isDeleted(cardId: self.selectedCardId) + .flatMapLatest { isDeleted -> Observable in + return .concat([ + .just(.willPushToWrite(isDeleted)), + .just(.updateIsDeleted(isDeleted)) + ]) + } case .resetPushState: return .just(.willPushToWrite(nil)) @@ -158,6 +165,8 @@ class DetailViewReactor: Reactor { func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { + case let .cardType(isFeed): + newState.isFeed = isFeed case let .detailCard(detailCard): newState.detailCard = detailCard case let .commentCards(commentCards): @@ -184,11 +193,11 @@ class DetailViewReactor: Reactor { func detailCard() -> Observable { - let coordinate = self.settingsUseCase.coordinate() + let coordinate = self.locationUseCase.coordinate() let latitude = coordinate.latitude let longitude = coordinate.longitude - return self.cardUseCase.detailCard( + return self.fetchCardDetailUseCase.detailCard( id: self.selectedCardId, latitude: latitude, longitude: longitude @@ -198,11 +207,11 @@ class DetailViewReactor: Reactor { func commentCards() -> Observable { - let coordinate = self.settingsUseCase.coordinate() + let coordinate = self.locationUseCase.coordinate() let latitude = coordinate.latitude let longitude = coordinate.longitude - return self.cardUseCase.commentCard( + return self.fetchCardDetailUseCase.commentCards( id: self.selectedCardId, lastId: nil, latitude: latitude, @@ -213,11 +222,11 @@ class DetailViewReactor: Reactor { func fetchMoreCommentCards(_ lastId: String) -> Observable { - let coordinate = self.settingsUseCase.coordinate() + let coordinate = self.locationUseCase.coordinate() let latitude = coordinate.latitude let longitude = coordinate.longitude - return self.cardUseCase.commentCard( + return self.fetchCardDetailUseCase.commentCards( id: self.selectedCardId, lastId: lastId, latitude: latitude, @@ -230,7 +239,7 @@ class DetailViewReactor: Reactor { extension DetailViewReactor { func reactorForPush(_ selectedId: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, .comment, type: .push, with: selectedId) + DetailViewReactor(dependencies: self.dependencies, with: selectedId) } func reactorForReport() -> ReportViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift index faa87b61..4f147962 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift @@ -97,10 +97,6 @@ class ReportViewController: BaseNavigationViewController, View { $0.trailing.equalToSuperview().offset(-16) $0.height.equalTo(56) } - } - - override func bind() { - super.bind() self.setupReportButtons() } @@ -126,6 +122,7 @@ class ReportViewController: BaseNavigationViewController, View { reactor.state.map(\.reportReason) .filterNil() .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, reportReason in let items = object.container.arrangedSubviews @@ -141,6 +138,7 @@ class ReportViewController: BaseNavigationViewController, View { reactor.state.map(\.isReported) .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.showSuccessReportedDialog() } @@ -148,10 +146,10 @@ class ReportViewController: BaseNavigationViewController, View { reactor.state.map(\.hasErrors) .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in - object.navigationPop { - NotificationCenter.default.post(name: .updatedReportState, object: nil, userInfo: nil) - } + NotificationCenter.default.post(name: .updatedReportState, object: nil, userInfo: nil) + object.navigationPop() } .disposed(by: self.disposeBag) } @@ -183,7 +181,7 @@ private extension ReportViewController { item.snp.makeConstraints { $0.height.equalTo(48) } - item.rx.throttleTap + item.rx.throttleTap(.seconds(2)) .map { _ in Reactor.Action.updateReportReason(reportType) } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -199,9 +197,8 @@ private extension ReportViewController { style: .primary, action: { UIApplication.topViewController?.dismiss(animated: true) { - self.navigationPop { - NotificationCenter.default.post(name: .updatedReportState, object: nil, userInfo: nil) - } + NotificationCenter.default.post(name: .updatedReportState, object: nil, userInfo: nil) + self.navigationPop() } } ) diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift index c6808266..0c6d719b 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift @@ -27,19 +27,18 @@ class ReportViewReactor: Reactor { fileprivate(set) var hasErrors: Bool } - var initialState: State + var initialState: State = .init(reportReason: nil, isReported: false, hasErrors: false) private let dependencies: AppDIContainerable - private let cardUseCase: CardUseCase + private let reportCardUseCase: ReportCardUseCase /// 신고할 카드 id private let id: String init(dependencies: AppDIContainerable, with id: String) { self.dependencies = dependencies - self.cardUseCase = dependencies.rootContainer.resolve(CardUseCase.self) - self.id = id + self.reportCardUseCase = dependencies.rootContainer.resolve(ReportCardUseCase.self) - self.initialState = State(reportReason: nil, isReported: false, hasErrors: false) + self.id = id } func mutate(action: Action) -> Observable { @@ -51,9 +50,9 @@ class ReportViewReactor: Reactor { guard let reportReason = self.currentState.reportReason else { return .empty() } - return self.cardUseCase.reportCard(id: self.id, reportType: reportReason.rawValue) + return self.reportCardUseCase.report(cardId: self.id, reportType: reportReason.rawValue) .map(Mutation.updateisReported) - .catch(self.catchClosure) + .catchAndReturn(.updateHasErrors(true)) } } @@ -73,14 +72,15 @@ class ReportViewReactor: Reactor { extension ReportViewReactor { - var catchClosure: ((Error) throws -> Observable ) { - return { error in - - let nsError = error as NSError - switch nsError.code { - case 409, 410: return .just(.updateHasErrors(true)) - default: return .empty() - } - } - } + // TODO: 임시, 에러 발생 시 뒤로가기 + // var catchClosure: ((Error) throws -> Observable ) { + // return { error in + // + // let nsError = error as NSError + // switch nsError.code { + // case 409, 410: return .just(.updateHasErrors(true)) + // default: return .empty() + // } + // } + // } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift index 4b6b520f..3871773c 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift @@ -185,7 +185,7 @@ class HomeViewController: BaseNavigationViewController, View { super.viewDidLoad() // 제스처 뒤로가기를 위한 델리게이트 설정 - self.navigationController?.interactivePopGestureRecognizer?.delegate = self + self.parent?.navigationController?.interactivePopGestureRecognizer?.delegate = self NotificationCenter.default.addObserver( self, @@ -194,13 +194,6 @@ class HomeViewController: BaseNavigationViewController, View { object: nil ) - NotificationCenter.default.addObserver( - self, - selector: #selector(self.reloadData(_:)), - name: .reloadData, - object: nil - ) - NotificationCenter.default.addObserver( self, selector: #selector(self.changedLocationAuthorization(_:)), @@ -264,23 +257,16 @@ class HomeViewController: BaseNavigationViewController, View { func bind(reactor: HomeViewReactor) { // navigation - self.rightAlamButton.rx.throttleTap() + self.rightAlamButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in let viewController = NotificationViewController() viewController.reactor = reactor.reactorForNotification() - object.navigationPush(viewController, animated: true, bottomBarHidden: true) - } - .disposed(by: self.disposeBag) - - // tabBar 표시 - self.rx.viewDidAppear - .subscribe(with: self) { object, _ in - object.hidesBottomBarWhenPushed = false + object.parent?.navigationPush(viewController, animated: true) } .disposed(by: self.disposeBag) // Action - self.rx.viewDidLoad + self.rx.viewDidAppear .map { _ in Reactor.Action.landing } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -306,12 +292,14 @@ class HomeViewController: BaseNavigationViewController, View { reactor.state.map(\.hasUnreadNotifications) .distinctUntilChanged() .map { $0 == false } + .observe(on: MainScheduler.asyncInstance) .bind(to: self.dotWithoutReadView.rx.isHidden) .disposed(by: self.disposeBag) reactor.state.map(\.noticeInfos) .filterNil() .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, noticeInfos in let models: [SOMPageModel] = noticeInfos.map { SOMPageModel(data: $0) } object.topNoticeView.frame = CGRect( @@ -323,19 +311,23 @@ class HomeViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) - let cardIsDeleted = reactor.state.map(\.cardIsDeleted).filterNil() + let cardIsDeleted = reactor.state.map(\.cardIsDeleted) + .distinctUntilChanged(reactor.canPushToDetail) + .filterNil() cardIsDeleted .filter { $0.isDeleted } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.showPungedCardDialog() } .disposed(by: self.disposeBag) cardIsDeleted .filter { $0.isDeleted == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, cardIsDeleted in let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail(with: cardIsDeleted.selectedId) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) { _ in + object.parent?.navigationPush(detailViewController, animated: true) { _ in reactor.action.onNext(.resetPushState) } } @@ -418,20 +410,10 @@ class HomeViewController: BaseNavigationViewController, View { self.tableView.setContentOffset(toTop, animated: true) } - @objc - private func reloadData(_ notification: Notification) { - - self.reactor?.action.onNext(.landing) - } - @objc private func changedLocationAuthorization(_ notification: Notification) { - if self.stickyTabBar.selectedIndex == 2, self.reactor?.initialState.hasPermission == false { - - self.reactor?.action.onNext(.refresh) - self.showLocationPermissionDialog() - } + self.reactor?.action.onNext(.updateLocationPermission) } } @@ -558,7 +540,7 @@ extension HomeViewController: SOMStickyTabBarDelegate { } self.reactor?.action.onNext(.updateDisplayType(displayType)) - if index == 2, self.reactor?.initialState.hasPermission == false { + if index == 2, self.reactor?.currentState.hasPermission == false { self.showLocationPermissionDialog() } } @@ -585,7 +567,7 @@ extension HomeViewController: SOMPageViewsDelegate { let viewController = NotificationViewController() viewController.reactor = reactorForNotification - self.navigationPush(viewController, animated: true, bottomBarHidden: true) + self.parent?.navigationPush(viewController, animated: true) } } @@ -706,7 +688,13 @@ extension HomeViewController: UITableViewDelegate { let isScrollingDown = delta > 0 // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.tableView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding @@ -738,8 +726,8 @@ extension HomeViewController: UITableViewDelegate { self.headerViewContainerTopConstraint?.update(offset: 0).update(priority: .high) } - UIView.animate(withDuration: 0.25) { - self.view.layoutIfNeeded() + UIView.animate(withDuration: 0.25) { [weak self] in + self?.view.layoutIfNeeded() } // 아래로 스크롤 중일 때, 데이터 추가로드 가능 diff --git a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift index f2027338..05ba28c4 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift @@ -25,6 +25,7 @@ class HomeViewReactor: Reactor { } enum Action: Equatable { + case updateLocationPermission case landing case refresh case moreFind(String) @@ -35,6 +36,7 @@ class HomeViewReactor: Reactor { } enum Mutation { + case updateLocationPermission(Bool) case cards([BaseCardInfo]) case more([BaseCardInfo]) case updateHasUnreadNotifications(Bool) @@ -61,18 +63,22 @@ class HomeViewReactor: Reactor { var initialState: State private let dependencies: AppDIContainerable - private let cardUseCase: CardUseCase + private let fetchCardUseCase: FetchCardUseCase + private let fetchCardDetailUseCase: FetchCardDetailUseCase + private let fetchNoticeUseCase: FetchNoticeUseCase private let notificationUseCase: NotificationUseCase - private let settingsUseCase: SettingsUseCase + private let locationUseCase: LocationUseCase init(dependencies: AppDIContainerable, displayType: DisplayType = .latest) { self.dependencies = dependencies - self.cardUseCase = dependencies.rootContainer.resolve(CardUseCase.self) + self.fetchCardUseCase = dependencies.rootContainer.resolve(FetchCardUseCase.self) + self.fetchCardDetailUseCase = dependencies.rootContainer.resolve(FetchCardDetailUseCase.self) + self.fetchNoticeUseCase = dependencies.rootContainer.resolve(FetchNoticeUseCase.self) self.notificationUseCase = dependencies.rootContainer.resolve(NotificationUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.locationUseCase = dependencies.rootContainer.resolve(LocationUseCase.self) self.initialState = State( - hasPermission: self.settingsUseCase.hasPermission(), + hasPermission: self.locationUseCase.hasPermission(), displayType: displayType, noticeInfos: nil, latestCards: nil, @@ -85,9 +91,11 @@ class HomeViewReactor: Reactor { ) } - func mutate(action: Action) -> Observable { switch action { + case .updateLocationPermission: + + return .just(.updateLocationPermission(self.locationUseCase.hasPermission())) case .landing: let displayType = self.currentState.displayType @@ -157,20 +165,11 @@ class HomeViewReactor: Reactor { ]) case let .detailCard(selectedId): - let coordinate = self.settingsUseCase.coordinate() - let latitude = coordinate.latitude - let longitude = coordinate.longitude - return .concat([ .just(.cardIsDeleted(nil)), - self.cardUseCase.detailCard( - id: selectedId, - latitude: latitude, - longitude: longitude - ) - .map { _ in (selectedId, false) } + self.fetchCardDetailUseCase.isDeleted(cardId: selectedId) + .map { (selectedId, $0) } .map(Mutation.cardIsDeleted) - .catchAndReturn(.cardIsDeleted((selectedId, true))) ]) case .resetPushState: @@ -181,6 +180,8 @@ class HomeViewReactor: Reactor { func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { + case let .updateLocationPermission(hasPermission): + newState.hasPermission = hasPermission case let .cards(cards): switch newState.displayType { case .latest: newState.latestCards = cards @@ -214,20 +215,24 @@ private extension HomeViewReactor { func refresh(_ displayType: DisplayType, _ distanceFilter: String) -> Observable { - let coordinate = self.settingsUseCase.coordinate() + let coordinate = self.locationUseCase.coordinate() let latitude = coordinate.latitude let longitude = coordinate.longitude switch displayType { case .latest: - return self.cardUseCase.latestCard(lastId: nil, latitude: latitude, longitude: longitude) - .map(Mutation.cards) + return self.fetchCardUseCase.latestCards( + lastId: nil, + latitude: latitude, + longitude: longitude + ) + .map(Mutation.cards) case .popular: - return self.cardUseCase.popularCard(latitude: latitude, longitude: longitude) + return self.fetchCardUseCase.popularCards(latitude: latitude, longitude: longitude) .map(Mutation.cards) case .distance: let distanceFilter = distanceFilter.replacingOccurrences(of: "km", with: "") - return self.cardUseCase.distanceCard( + return self.fetchCardUseCase.distanceCards( lastId: nil, latitude: latitude, longitude: longitude, @@ -239,17 +244,21 @@ private extension HomeViewReactor { func moreFind(_ lastId: String) -> Observable { - let coordinate = self.settingsUseCase.coordinate() + let coordinate = self.locationUseCase.coordinate() let latitude = coordinate.latitude let longitude = coordinate.longitude switch self.currentState.displayType { case .latest: - return self.cardUseCase.latestCard(lastId: lastId, latitude: latitude, longitude: longitude) - .map(Mutation.more) + return self.fetchCardUseCase.latestCards( + lastId: lastId, + latitude: latitude, + longitude: longitude + ) + .map(Mutation.more) case .distance: let distanceFilter = self.currentState.distanceFilter.replacingOccurrences(of: "km", with: "") - return self.cardUseCase.distanceCard( + return self.fetchCardUseCase.distanceCards( lastId: lastId, latitude: latitude, longitude: longitude, @@ -263,12 +272,13 @@ private extension HomeViewReactor { func unreadNotifications() -> Observable { - return self.notificationUseCase.notices(lastId: nil, size: 3, requestType: .notification) + return self.fetchNoticeUseCase.notices(lastId: nil, size: 3, requestType: .notification) .flatMapLatest { noticeInfos -> Observable in return .concat([ - self.notificationUseCase.unreadNotifications(lastId: nil) - .map { .updateHasUnreadNotifications($0.isEmpty == false && noticeInfos.isEmpty == false) }, + self.notificationUseCase.isUnreadNotiEmpty() + .map { !$0 } + .map(Mutation.updateHasUnreadNotifications), .just(.notices(noticeInfos)) ]) } @@ -314,6 +324,14 @@ extension HomeViewReactor { prevDisplayState.populars == currDisplayState.populars && prevDisplayState.distances == currDisplayState.distances } + + func canPushToDetail( + prev prevCardIsDeleted: (selectedId: String, isDeleted: Bool)?, + curr currCardIsDeleted: (selectedId: String, isDeleted: Bool)? + ) -> Bool { + return prevCardIsDeleted?.selectedId == currCardIsDeleted?.selectedId && + prevCardIsDeleted?.isDeleted == currCardIsDeleted?.isDeleted + } } @@ -324,6 +342,6 @@ extension HomeViewReactor { } func reactorForDetail(with id: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, .feed, with: id) + DetailViewReactor(dependencies: self.dependencies, with: id) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift index 68a55b64..7041ac1b 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift @@ -180,6 +180,7 @@ class NotificationViewController: BaseNavigationViewController, View { reactor.state.map(\.displayType) .filter { $0 == .notice } .take(1) + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self.headerView) { headerView, _ in headerView.didSelectTabBarItem(1, onlyUpdateApperance: true) } @@ -313,10 +314,9 @@ extension NotificationViewController: UITableViewDelegate { let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail( - entranceType: .feed, with: notification.targetCardId ) - self.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + self.navigationPush(detailViewController, animated: true) default: return } @@ -326,17 +326,16 @@ extension NotificationViewController: UITableViewDelegate { let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail( - entranceType: .feed, with: notification.targetCardId ) - self.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + self.navigationPush(detailViewController, animated: true) case let .follow(notification): reactor.action.onNext(.requestRead(notification.notificationInfo.notificationId)) let profileViewController = ProfileViewController() profileViewController.reactor = reactor.reactorForProfile(with: notification.userId) - self.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + self.navigationPush(profileViewController, animated: true) default: return } @@ -347,23 +346,21 @@ extension NotificationViewController: UITableViewDelegate { let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail( - entranceType: .feed, with: notification.targetCardId ) - self.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + self.navigationPush(detailViewController, animated: true) case let .tag(notification): let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail( - entranceType: .feed, with: notification.targetCardId ) - self.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + self.navigationPush(detailViewController, animated: true) case let .follow(notification): let profileViewController = ProfileViewController() profileViewController.reactor = reactor.reactorForProfile(with: notification.userId) - self.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + self.navigationPush(profileViewController, animated: true) default: return } @@ -532,7 +529,13 @@ extension NotificationViewController: UITableViewDelegate { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.tableView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift index 7389fca6..21bd7c5d 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift @@ -42,10 +42,12 @@ class NotificationViewReactor: Reactor { private let dependencies: AppDIContainerable private let notificationUseCase: NotificationUseCase + private let fetchNoticeUseCase: FetchNoticeUseCase init(dependencies: AppDIContainerable, displayType: DisplayType = .activity(.unread)) { self.dependencies = dependencies self.notificationUseCase = dependencies.rootContainer.resolve(NotificationUseCase.self) + self.fetchNoticeUseCase = dependencies.rootContainer.resolve(FetchNoticeUseCase.self) self.initialState = State( displayType: displayType, @@ -68,7 +70,7 @@ class NotificationViewReactor: Reactor { ) .map(Mutation.notifications) .catch(self.catchClosureNotis), - self.notificationUseCase.notices(lastId: nil, size: 10, requestType: .notification) + self.fetchNoticeUseCase.notices(lastId: nil, size: 10, requestType: .notification) .map(Mutation.notices) .catch(self.catchClosureNotices) ]) @@ -90,7 +92,7 @@ class NotificationViewReactor: Reactor { case .notice: return .concat([ .just(.updateIsRefreshing(true)), - self.notificationUseCase.notices(lastId: nil, size: 10, requestType: .notification) + self.fetchNoticeUseCase.notices(lastId: nil, size: 10, requestType: .notification) .map(Mutation.notices) .catch(self.catchClosureNotices), .just(.updateIsRefreshing(false)) @@ -109,7 +111,7 @@ class NotificationViewReactor: Reactor { ]) case .notice: return .concat([ - self.notificationUseCase.notices(lastId: lastId, size: 10, requestType: .notification) + self.fetchNoticeUseCase.notices(lastId: lastId, size: 10, requestType: .notification) .map(Mutation.moreNotices) .catch(self.catchClosureNoticesMore) ]) @@ -263,8 +265,8 @@ extension NotificationViewReactor { extension NotificationViewReactor { - func reactorForDetail(entranceType: EntranceCardType, with id: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, entranceType, type: .navi, with: id) + func reactorForDetail(with id: String) -> DetailViewReactor { + DetailViewReactor(dependencies: self.dependencies, with: id) } func reactorForProfile(with userId: String) -> ProfileViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift index eb5217b9..7dd629d2 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift @@ -47,6 +47,8 @@ class NotificationViewCell: UITableViewCell { private let contentLabel = UILabel().then { $0.textColor = .som.v2.gray600 + $0.lineBreakMode = .byWordWrapping + $0.lineBreakStrategy = .hangulWordPriority $0.numberOfLines = 0 $0.textAlignment = .left } diff --git a/SOOUM/SOOUM/Presentations/Main/MainTabBarController.swift b/SOOUM/SOOUM/Presentations/Main/MainTabBarController.swift index 177ba23e..10dfb1e9 100644 --- a/SOOUM/SOOUM/Presentations/Main/MainTabBarController.swift +++ b/SOOUM/SOOUM/Presentations/Main/MainTabBarController.swift @@ -15,7 +15,6 @@ import RxSwift import SnapKit import Then - class MainTabBarController: SOMTabBarController, View { enum Constants { @@ -142,7 +141,7 @@ class MainTabBarController: SOMTabBarController, View { .bind(to: reactor.action) .disposed(by: self.disposeBag) - self.willPushWriteCard + self.willPushWriteCard.throttle(.seconds(3), scheduler: MainScheduler.instance) .map { _ in Reactor.Action.postingPermission } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -154,49 +153,30 @@ class MainTabBarController: SOMTabBarController, View { .subscribe(with: self) { object, profileInfo in switch reactor.currentState.entranceType { - case .pushToFeedDetail: - - guard let navigationController = object.viewControllers[0] as? UINavigationController, - let homeViewController = navigationController.viewControllers.first as? HomeViewController, - let targetCardId = reactor.pushInfo?.targetCardId - else { return } + case .pushToDetail: object.didSelectedIndex(0) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak object] in - object?.setupDetailViewController( - homeViewController, - with: reactor.reactorForDetail(targetCardId, type: .feed), - completion: { reactor.action.onNext(.resetEntrance) } - ) - } - case .pushToCommentDetail: - - guard let navigationController = object.viewControllers[0] as? UINavigationController, - let homeViewController = navigationController.viewControllers.first as? HomeViewController, + guard let selectedViewController = object.selectedViewController, let targetCardId = reactor.pushInfo?.targetCardId else { return } - object.didSelectedIndex(0) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak object] in object?.setupDetailViewController( - homeViewController, - with: reactor.reactorForDetail(targetCardId, type: .comment), + selectedViewController, + with: reactor.reactorForDetail(targetCardId), completion: { reactor.action.onNext(.resetEntrance) } ) } case .pushToNotification: - guard let navigationController = object.viewControllers[0] as? UINavigationController, - let homeViewController = navigationController.viewControllers.first as? HomeViewController - else { return } - object.didSelectedIndex(0) + guard let selectedViewController = object.selectedViewController else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak object] in object?.setupNotificationViewController( - homeViewController, + selectedViewController, with: reactor.reactorForNoti(), completion: { reactor.action.onNext(.resetEntrance) } ) @@ -205,15 +185,14 @@ class MainTabBarController: SOMTabBarController, View { object.didSelectedIndex(2) - guard let navigationController = object.viewControllers[2] as? UINavigationController, - let tagViewController = navigationController.viewControllers.first as? TagViewController, + guard let selectedViewController = object.selectedViewController, let targetCardId = reactor.pushInfo?.targetCardId else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak object] in object?.setupTagDetailViewController( - tagViewController, - with: reactor.reactorForDetail(targetCardId, type: .feed), + selectedViewController, + with: reactor.reactorForDetail(targetCardId), completion: { reactor.action.onNext(.resetEntrance) } ) } @@ -221,13 +200,11 @@ class MainTabBarController: SOMTabBarController, View { object.didSelectedIndex(3) - guard let navigationController = object.viewControllers[3] as? UINavigationController, - let profileViewController = navigationController.viewControllers.first as? ProfileViewController - else { return } + guard let selectedViewController = object.selectedViewController else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak object] in object?.setupFollowViewController( - profileViewController, + selectedViewController, with: reactor.reactorForFollow(nickname: profileInfo.nickname, with: profileInfo.userId), completion: { reactor.action.onNext(.resetEntrance) } ) @@ -238,8 +215,8 @@ class MainTabBarController: SOMTabBarController, View { let window: UIWindow = windowScene.windows.first(where: { $0.isKeyWindow }) else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - object.setupLaunchScreenViewController( + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak object] in + object?.setupLaunchScreenViewController( window, with: reactor.reactorForLaunchScreen() ) @@ -251,9 +228,10 @@ class MainTabBarController: SOMTabBarController, View { } .disposed(by: self.disposeBag) - let couldPosting = reactor.state.map(\.couldPosting).distinctUntilChanged().filterNil() + let couldPosting = reactor.pulse(\.$couldPosting).distinctUntilChanged().filterNil() couldPosting .filter { $0.isBaned == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.hasFirstLaunchGuide = false @@ -273,6 +251,7 @@ class MainTabBarController: SOMTabBarController, View { couldPosting .filter { $0.isBaned } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, postingPermission in let banEndGapToDays = postingPermission.expiredAt?.infoReadableTimeTakenFromThisForBanEndPosting(to: Date().toKorea()) @@ -339,23 +318,6 @@ private extension MainTabBarController { actions: [confirmAction] ) } - - func showPrepare() { - - let confirmAction = SOMDialogAction( - title: "확인", - style: .primary, - action: { - UIApplication.topViewController?.dismiss(animated: true) - } - ) - - SOMDialogViewController.show( - title: "서비스 준비중입니다.", - message: "추후 개발 완료되면 사용가능합니다.", - actions: [confirmAction] - ) - } } @@ -364,65 +326,61 @@ private extension MainTabBarController { private extension MainTabBarController { func setupDetailViewController( - _ homeViewController: HomeViewController, + _ selectedViewController: UIViewController, with reactor: DetailViewReactor, completion: @escaping (() -> Void) ) { let detailViewController = DetailViewController() detailViewController.reactor = reactor - homeViewController.navigationPush( + selectedViewController.navigationPush( detailViewController, animated: true, - bottomBarHidden: true, completion: { _ in completion() } ) } func setupNotificationViewController( - _ homeViewController: HomeViewController, + _ selectedViewController: UIViewController, with reactor: NotificationViewReactor, completion: @escaping (() -> Void) ) { let notificationViewController = NotificationViewController() notificationViewController.reactor = reactor - homeViewController.navigationPush( + selectedViewController.navigationPush( notificationViewController, animated: true, - bottomBarHidden: true, completion: { _ in completion() } ) } func setupTagDetailViewController( - _ tagViewController: TagViewController, + _ selectedViewController: UIViewController, with reactor: DetailViewReactor, completion: @escaping (() -> Void) ) { let detailViewController = DetailViewController() detailViewController.reactor = reactor - tagViewController.navigationPush( + selectedViewController.navigationPush( detailViewController, animated: true, - bottomBarHidden: true, completion: { _ in completion() } ) } func setupFollowViewController( - _ profileViewController: ProfileViewController, + _ selectedViewController: UIViewController, with reactor: FollowViewReactor, completion: @escaping (() -> Void) ) { let followViewController = FollowViewController() followViewController.reactor = reactor - profileViewController.navigationPush( + selectedViewController.navigationPush( followViewController, animated: true, - bottomBarHidden: true, completion: { _ in completion() } ) } diff --git a/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift b/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift index d7cd1829..2e0a6567 100644 --- a/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift @@ -13,10 +13,8 @@ class MainTabBarReactor: Reactor { enum EntranceType { /// 푸시 알림(알림 화면)으로 진입할 경우 case pushToNotification - /// 푸시 알림(피드 상세 화면)으로 진입할 경우 - case pushToFeedDetail - /// 푸시 알림(댓글 카드 상세 화면)으로 진입할 경우 - case pushToCommentDetail + /// 푸시 알림(상세 화면)으로 진입할 경우 + case pushToDetail /// 푸시 알림(피드 상세 화면 + 태그 탭)으로 진입할 경우 case pushToTagDetail /// 푸시 알림(내 팔로우 화면 + 팔로우 탭)으로 진입할 경우 @@ -44,7 +42,7 @@ class MainTabBarReactor: Reactor { struct State { fileprivate(set) var entranceType: EntranceType - fileprivate(set) var couldPosting: PostingPermission? + @Pulse fileprivate(set) var couldPosting: PostingPermission? @Pulse fileprivate(set) var profileInfo: ProfileInfo? } @@ -53,25 +51,28 @@ class MainTabBarReactor: Reactor { var pushInfo: PushNotificationInfo? private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase + private let fetchUserInfoUseCase: FetchUserInfoUseCase + private let validateUserUseCase: ValidateUserUseCase private let notificationUseCase: NotificationUseCase - private let settingsUseCase: SettingsUseCase + private let updateNotifyUseCase: UpdateNotifyUseCase + private let locationUseCase: LocationUseCase init(dependencies: AppDIContainerable, pushInfo: PushNotificationInfo? = nil) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.fetchUserInfoUseCase = dependencies.rootContainer.resolve(FetchUserInfoUseCase.self) + self.validateUserUseCase = dependencies.rootContainer.resolve(ValidateUserUseCase.self) self.notificationUseCase = dependencies.rootContainer.resolve(NotificationUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.updateNotifyUseCase = dependencies.rootContainer.resolve(UpdateNotifyUseCase.self) + self.locationUseCase = dependencies.rootContainer.resolve(LocationUseCase.self) var willNavigate: EntranceType { switch pushInfo?.notificationType { - case .feedLike: return .pushToFeedDetail - case .commentLike, .commentWrite: return .pushToCommentDetail - case .blocked, .deleted: return .pushToNotification - case .tagUsage: return .pushToTagDetail - case .follow: return .pushToFollow - case .transferSuccess: return .pushToLaunchScreen - default: return .none + case .feedLike, .commentLike, .commentWrite: return .pushToDetail + case .blocked, .deleted: return .pushToNotification + case .tagUsage: return .pushToTagDetail + case .follow: return .pushToFollow + case .transferSuccess: return .pushToLaunchScreen + default: return .none } } self.pushInfo = pushInfo @@ -87,35 +88,31 @@ class MainTabBarReactor: Reactor { switch action { case .requestLocationPermission: - if self.settingsUseCase.checkLocationAuthStatus() == .notDetermined { - self.settingsUseCase.requestLocationPermission() + if self.locationUseCase.checkLocationAuthStatus() == .notDetermined { + self.locationUseCase.requestLocationPermission() } - return .empty() + return self.updateNotifyUseCase.switchNotification(on: true) + .flatMapLatest { _ -> Observable in .empty() } case .judgeEntrance: - if let pushInfo = self.pushInfo { - - return .concat([ - self.userUseCase.profile(userId: nil) - .flatMapLatest { profileInfo -> Observable in - - return self.notificationUseCase.requestRead( - notificationId: pushInfo.notificationId ?? "" - ) - .map { _ in .updateEntrance(profileInfo) } - }, - self.settingsUseCase.switchNotification(on: true) - .flatMapLatest { _ -> Observable in .empty() } - ]) - } else { - - return self.settingsUseCase.switchNotification(on: true) - .flatMapLatest { _ -> Observable in .empty() } - } + guard let pushInfo = self.pushInfo else { return .empty() } + + return self.fetchUserInfoUseCase.userInfo(userId: nil) + .flatMapLatest { profileInfo -> Observable in + + if let notificationId = pushInfo.notificationId { + + return self.notificationUseCase.requestRead(notificationId: notificationId) + .map { _ in .updateEntrance(profileInfo) } + } else { + + return .just(.updateEntrance(profileInfo)) + } + } case .postingPermission: - return self.userUseCase.postingPermission() + return self.validateUserUseCase.postingPermission() .map(Mutation.updatePostingPermission) case .resetCouldPosting: @@ -159,15 +156,15 @@ extension MainTabBarReactor { } func reactorForProfile() -> ProfileViewReactor { - ProfileViewReactor(dependencies: self.dependencies, type: .myWithNavi) + ProfileViewReactor(dependencies: self.dependencies, type: .my) } func reactorForNoti() -> NotificationViewReactor { NotificationViewReactor(dependencies: self.dependencies) } - func reactorForDetail(_ targetCardId: String, type: EntranceCardType) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, type, type: .push, with: targetCardId) + func reactorForDetail(_ targetCardId: String) -> DetailViewReactor { + DetailViewReactor(dependencies: self.dependencies, with: targetCardId) } func reactorForFollow(nickname: String, with userId: String) -> FollowViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileUserViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileUserViewCell.swift index 192d2f43..9a59065f 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileUserViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileUserViewCell.swift @@ -38,9 +38,7 @@ class ProfileUserViewCell: UICollectionViewCell { $0.spacing = 2 } - private let visitedCountContainer = UIView().then { - $0.isHidden = true - } + private let visitedCountContainer = UIView() private let totalVisitedTitleLabel = UILabel().then { $0.text = Text.totalVisitedTitle @@ -246,7 +244,6 @@ class ProfileUserViewCell: UICollectionViewCell { self.model = model - self.visitedCountContainer.isHidden = model.cardCnt == "0" self.totalVisitedCountLabel.text = model.totalVisitCnt self.totalVisitedCountLabel.typography = .som.v2.caption2 self.todayVisitedCountLabel.text = model.todayVisitCnt diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewController.swift index bb106fb1..8419a5f2 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewController.swift @@ -90,7 +90,7 @@ class FollowViewController: BaseNavigationViewController, View { cell.setModel(follower) - cell.profileBackgroundButton.rx.throttleTap + cell.profileBackgroundButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in if follower.isRequester { guard let navigationController = object.navigationController, @@ -99,7 +99,7 @@ class FollowViewController: BaseNavigationViewController, View { if navigationController.viewControllers.first?.isKind(of: ProfileViewController.self) == true { - object.navigationPopToRoot(animated: false, bottomBarHidden: false) + object.navigationPopToRoot(animated: false) } else { tabBarController.didSelectedIndex(3) @@ -108,7 +108,7 @@ class FollowViewController: BaseNavigationViewController, View { } else { let profileViewController = ProfileViewController() profileViewController.reactor = reactor.reactorForProfile(follower.memberId) - object.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + object.navigationPush(profileViewController, animated: true) } } .disposed(by: cell.disposeBag) @@ -139,11 +139,11 @@ class FollowViewController: BaseNavigationViewController, View { cell.setModel(following) - cell.profileBackgroundButton.rx.throttleTap + cell.profileBackgroundButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in let profileViewController = ProfileViewController() profileViewController.reactor = reactor.reactorForProfile(following.memberId) - object.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + object.navigationPush(profileViewController, animated: true) } .disposed(by: cell.disposeBag) @@ -166,7 +166,7 @@ class FollowViewController: BaseNavigationViewController, View { cell.setModel(following) - cell.profileBackgroundButton.rx.throttleTap + cell.profileBackgroundButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in if following.isRequester { guard let navigationController = object.navigationController, @@ -175,7 +175,7 @@ class FollowViewController: BaseNavigationViewController, View { if navigationController.viewControllers.first?.isKind(of: ProfileViewController.self) == true { - object.navigationPopToRoot(animated: false, bottomBarHidden: false) + object.navigationPopToRoot(animated: false) } else { tabBarController.didSelectedIndex(3) @@ -184,7 +184,7 @@ class FollowViewController: BaseNavigationViewController, View { } else { let profileViewController = ProfileViewController() profileViewController.reactor = reactor.reactorForProfile(following.memberId) - object.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + object.navigationPush(profileViewController, animated: true) } } .disposed(by: cell.disposeBag) @@ -262,18 +262,6 @@ class FollowViewController: BaseNavigationViewController, View { } } - override func bind() { - // 뒤로가기 시 상대 팔로우 화면이면 하단 네비바 숨김 - self.navigationBar.backButton.rx.throttleTap - .subscribe(with: self) { object, _ in - object.navigationPop( - animated: true, - bottomBarHidden: object.reactor?.viewType == .other - ) - } - .disposed(by: self.disposeBag) - } - // MARK: ReactorKit - bind @@ -283,7 +271,10 @@ class FollowViewController: BaseNavigationViewController, View { let viewDidLoad = self.rx.viewDidLoad.share() viewDidLoad .subscribe(with: self.stickyTabBar) { stickyTabBar, _ in - stickyTabBar.didSelectTabBarItem(reactor.entranceType == .follower ? 0 : 1) + stickyTabBar.didSelectTabBarItem( + reactor.entranceType == .follower ? 0 : 1, + with: false + ) } .disposed(by: self.disposeBag) @@ -476,7 +467,13 @@ extension FollowViewController: UITableViewDelegate { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.tableView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift index d6ffb5fc..ceaa99cc 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift @@ -52,7 +52,8 @@ class FollowViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase + private let fetchFollowUseCase: FetchFollowUseCase + private let updateFollowUseCase: UpdateFollowUseCase let entranceType: EntranceType let viewType: ViewType @@ -67,7 +68,9 @@ class FollowViewReactor: Reactor { with userId: String ) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.fetchFollowUseCase = dependencies.rootContainer.resolve(FetchFollowUseCase.self) + self.updateFollowUseCase = dependencies.rootContainer.resolve(UpdateFollowUseCase.self) + self.entranceType = entranceType self.viewType = viewType self.nickname = nickname @@ -78,20 +81,23 @@ class FollowViewReactor: Reactor { switch action { case .landing: - return self.refresh() + return .concat([ + self.followers(with: nil) + .catchAndReturn(.followers([])), + self.followings(with: nil) + .catchAndReturn(.followings([])) + ]) case .refresh: let emit = self.currentState.followType == .follower ? - self.userUseCase.followers(userId: self.userId, lastId: nil) - .map(Mutation.followers) + self.followers(with: nil) .catch { _ in return .concat([ .just(.updateIsRefreshing(false)), .just(.followers([])) ]) } : - self.userUseCase.followings(userId: self.userId, lastId: nil) - .map(Mutation.followings) + self.followings(with: nil) .catch { _ in return .concat([ .just(.updateIsRefreshing(false)), @@ -106,12 +112,7 @@ class FollowViewReactor: Reactor { ]) case let .moreFind(type, lastId): - let emit = type == .follower ? - self.userUseCase.followers(userId: self.userId, lastId: lastId) - .map(Mutation.moreFollowers) : - self.userUseCase.followings(userId: self.userId, lastId: lastId) - .map(Mutation.moreFollowings) - + let emit = type == .follower ? self.followers(with: lastId) : self.followings(with: lastId) return emit case let .updateFollowType(followType): @@ -120,7 +121,7 @@ class FollowViewReactor: Reactor { return .concat([ .just(.updateIsUpdated(nil)), - self.userUseCase.updateFollowing(userId: userId, isFollow: isFollow) + self.updateFollowUseCase.updateFollowing(userId: userId, isFollow: isFollow) .map(Mutation.updateIsUpdated) ]) } @@ -149,15 +150,17 @@ class FollowViewReactor: Reactor { } private extension FollowViewReactor { + + func followers(with lastId: String?) -> Observable { + + return self.fetchFollowUseCase.followers(userId: self.userId, lastId: lastId) + .map { lastId == nil ? .followers($0) : .moreFollowers($0) } + } - func refresh() -> Observable { + func followings(with lastId: String?) -> Observable { - return .concat([ - self.userUseCase.followers(userId: self.userId, lastId: nil) - .map(Mutation.followers), - self.userUseCase.followings(userId: self.userId, lastId: nil) - .map(Mutation.followings) - ]) + return self.fetchFollowUseCase.followings(userId: self.userId, lastId: lastId) + .map { lastId == nil ? .followings($0) : .moreFollowings($0) } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift index bb96036c..0e1917bf 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift @@ -75,8 +75,7 @@ class ProfileViewController: BaseNavigationViewController, View { ).then { $0.backgroundColor = .som.v2.white - $0.contentInset.top = 0 - $0.contentInset.bottom = 54 + 16 + $0.contentInset = .zero $0.contentInsetAdjustmentBehavior = .never @@ -121,7 +120,20 @@ class ProfileViewController: BaseNavigationViewController, View { cell.cardContainerDidTap .subscribe(with: self) { object, _ in - object.collectionView.setContentOffset(CGPoint(x: 0, y: 84 + 76 + 48 + 16), animated: true) + switch reactor.currentState.cardType { + case .feed: + guard reactor.currentState.feedCardInfos.isEmpty == false else { return } + object.collectionView.setContentOffset( + CGPoint(x: 0, y: 84 + 76 + 48 + 16), + animated: true + ) + case .comment: + guard reactor.currentState.commentCardInfos.isEmpty == false else { return } + object.collectionView.setContentOffset( + CGPoint(x: 0, y: 84 + 76 + 48 + 16), + animated: true + ) + } } .disposed(by: cell.disposeBag) @@ -134,7 +146,8 @@ class ProfileViewController: BaseNavigationViewController, View { nickname: profileInfo.nickname, with: profileInfo.userId ) - object.navigationPush(followViewController, animated: true, bottomBarHidden: true) + let base = profileInfo.isAlreadyFollowing == nil ? object.parent : object + base?.navigationPush(followViewController, animated: true) } .disposed(by: cell.disposeBag) @@ -147,7 +160,8 @@ class ProfileViewController: BaseNavigationViewController, View { nickname: profileInfo.nickname, with: profileInfo.userId ) - object.navigationPush(followViewController, animated: true, bottomBarHidden: true) + let base = profileInfo.isAlreadyFollowing == nil ? object.parent : object + base?.navigationPush(followViewController, animated: true) } .disposed(by: cell.disposeBag) @@ -160,9 +174,10 @@ class ProfileViewController: BaseNavigationViewController, View { let updateProfileViewController = UpdateProfileViewController() updateProfileViewController.reactor = reactor.reactorForUpdate( nickname: profileInfo.nickname, - image: profileImage + image: profileImage, + imageName: profileInfo.profileImgName ) - object?.navigationPush(updateProfileViewController, animated: true, bottomBarHidden: true) + object?.parent?.navigationPush(updateProfileViewController, animated: true) } } .disposed(by: cell.disposeBag) @@ -198,10 +213,12 @@ class ProfileViewController: BaseNavigationViewController, View { } cell.cardDidTap + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, selectedId in let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail(selectedId) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + let base = reactor.entranceType == .my ? object.parent : object + base?.navigationPush(detailViewController, animated: true) } .disposed(by: cell.disposeBag) @@ -263,7 +280,7 @@ class ProfileViewController: BaseNavigationViewController, View { guard let reactor = self.reactor else { return } - let isMine = reactor.entranceType == .my || reactor.entranceType == .myWithNavi + let isMine = reactor.entranceType == .my self.navigationBar.hidesBackButton = isMine self.navigationBar.title = isMine ? Text.navigationTitle : nil @@ -286,9 +303,7 @@ class ProfileViewController: BaseNavigationViewController, View { super.viewDidLoad() // 제스처 뒤로가기를 위한 델리게이트 설정 - if self.reactor?.entranceType != .other { - self.navigationController?.interactivePopGestureRecognizer?.delegate = self - } + self.parent?.navigationController?.interactivePopGestureRecognizer?.delegate = self NotificationCenter.default.addObserver( self, @@ -304,11 +319,11 @@ class ProfileViewController: BaseNavigationViewController, View { func bind(reactor: ProfileViewReactor) { // 설정 화면 전환 - self.rightSettingButton.rx.throttleTap + self.rightSettingButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in let settingsViewController = SettingsViewController() settingsViewController.reactor = reactor.reactorForSettings() - object.navigationPush(settingsViewController, animated: true, bottomBarHidden: true) + object.parent?.navigationPush(settingsViewController, animated: true) } .disposed(by: self.disposeBag) @@ -342,13 +357,6 @@ class ProfileViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) - // tabBar 표시 - self.rx.viewDidAppear - .subscribe(with: self) { object, _ in - object.hidesBottomBarWhenPushed = reactor.entranceType == .other - } - .disposed(by: self.disposeBag) - // Action self.rx.viewDidLoad .map { _ in Reactor.Action.landing } @@ -378,7 +386,7 @@ class ProfileViewController: BaseNavigationViewController, View { cardType: $0.cardType, profileInfo: $0.profileInfo, feedCardInfos: $0.feedCardInfos, - commCardInfos: $0.commentCardInfos + commentCardInfos: $0.commentCardInfos ) } .distinctUntilChanged(reactor.canUpdateCells) @@ -400,7 +408,7 @@ class ProfileViewController: BaseNavigationViewController, View { let cardItem = Item.card( type: displayStates.cardType, feed: displayStates.feedCardInfos, - comment: displayStates.commCardInfos.isEmpty ? nil : displayStates.commCardInfos + comment: displayStates.commentCardInfos.isEmpty ? nil : displayStates.commentCardInfos ) snapshot.appendItems([cardItem], toSection: .card) @@ -411,7 +419,6 @@ class ProfileViewController: BaseNavigationViewController, View { reactor.pulse(\.$isBlocked) .filterNil() .filter { $0 } - .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in reactor.action.onNext(.updateCards) } @@ -420,7 +427,6 @@ class ProfileViewController: BaseNavigationViewController, View { reactor.pulse(\.$isFollowing) .filterNil() .filter { $0 } - .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in reactor.action.onNext(.updateProfile) } @@ -440,7 +446,11 @@ class ProfileViewController: BaseNavigationViewController, View { let itemHeight = (self.collectionView.bounds.width - 2) / 3 let newHeight = (numberOfRows * itemHeight) + ((numberOfRows - 1) * lineSpacing) - return max(newHeight, self.collectionView.bounds.height - 56) + let cellHeight: CGFloat = 84 + 76 + 48 + 16 + let headerHeight: CGFloat = 56 + let defaultHeight: CGFloat = collectionView.bounds.height - cellHeight - headerHeight + + return max(newHeight, defaultHeight) } @@ -575,14 +585,16 @@ extension ProfileViewController: UICollectionViewDelegateFlowLayout { switch reactor.currentState.cardType { case .feed: - return feeds.isEmpty ? - defaultHeight : - self.updateCollectionViewHeight(numberOfItems: feeds.count) + let newHeight = self.updateCollectionViewHeight(numberOfItems: feeds.count) + collectionView.contentInset.bottom = defaultHeight <= newHeight ? 88 + 16 : 0 + + return feeds.isEmpty ? defaultHeight : newHeight case .comment: - return comments.isEmpty ? - defaultHeight : - self.updateCollectionViewHeight(numberOfItems: comments.count) + let newHeight = self.updateCollectionViewHeight(numberOfItems: comments.count) + collectionView.contentInset.bottom = defaultHeight <= newHeight ? 88 + 16 : 0 + + return comments.isEmpty ? defaultHeight : newHeight } } @@ -608,7 +620,13 @@ extension ProfileViewController: UICollectionViewDelegateFlowLayout { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.collectionView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewReactor.swift index 17cbadcd..29c274a8 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewReactor.swift @@ -15,7 +15,7 @@ class ProfileViewReactor: Reactor { let cardType: EntranceCardType let profileInfo: ProfileInfo? let feedCardInfos: [ProfileCardInfo] - let commCardInfos: [ProfileCardInfo] + let commentCardInfos: [ProfileCardInfo] } enum Action: Equatable { @@ -62,14 +62,22 @@ class ProfileViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase + private let fetchUserInfoUseCase: FetchUserInfoUseCase + private let fetchCardUseCase: FetchCardUseCase + private let blockUserUseCase: BlockUserUseCase + private let updateFollowUseCase: UpdateFollowUseCase + let entranceType: EntranceType private let userId: String? init(dependencies: AppDIContainerable, type entranceType: EntranceType, with userId: String? = nil) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.fetchUserInfoUseCase = dependencies.rootContainer.resolve(FetchUserInfoUseCase.self) + self.fetchCardUseCase = dependencies.rootContainer.resolve(FetchCardUseCase.self) + self.blockUserUseCase = dependencies.rootContainer.resolve(BlockUserUseCase.self) + self.updateFollowUseCase = dependencies.rootContainer.resolve(UpdateFollowUseCase.self) + self.entranceType = entranceType self.userId = userId } @@ -78,7 +86,7 @@ class ProfileViewReactor: Reactor { switch action { case .landing: - return self.userUseCase.profile(userId: self.userId) + return self.fetchUserInfoUseCase.userInfo(userId: self.userId) .withUnretained(self) .flatMapLatest { object, profileInfo -> Observable in @@ -86,7 +94,7 @@ class ProfileViewReactor: Reactor { return .concat([ .just(.profile(profileInfo)), - object.userUseCase.feedCards(userId: profileInfo.userId, lastId: nil) + object.fetchCardUseCase.writtenFeedCards(userId: profileInfo.userId, lastId: nil) .map(Mutation.feedCardInfos) ]) } else { @@ -95,9 +103,9 @@ class ProfileViewReactor: Reactor { return .concat([ .just(.profile(profileInfo)), - object.userUseCase.feedCards(userId: profileInfo.userId, lastId: nil) + object.fetchCardUseCase.writtenFeedCards(userId: profileInfo.userId, lastId: nil) .map(Mutation.feedCardInfos), - object.userUseCase.myCommentCards(lastId: nil) + object.fetchCardUseCase.writtenCommentCards(lastId: nil) .map(Mutation.commentCardInfos) ]) } @@ -105,11 +113,11 @@ class ProfileViewReactor: Reactor { case .refresh: - return self.userUseCase.profile(userId: self.userId) + return self.fetchUserInfoUseCase.userInfo(userId: self.userId) .withUnretained(self) .flatMapLatest { object, profileInfo -> Observable in - if object.entranceType == .my || object.entranceType == .myWithNavi { + if object.entranceType == .my { // 사용자 닉네임 업데이트 UserDefaults.standard.nickname = profileInfo.nickname } @@ -120,7 +128,7 @@ class ProfileViewReactor: Reactor { .just(.updateIsRefreshing(true)), .just(.profile(profileInfo)) .catch(self.catchClosure), - object.userUseCase.feedCards(userId: profileInfo.userId, lastId: nil) + object.fetchCardUseCase.writtenFeedCards(userId: profileInfo.userId, lastId: nil) .map(Mutation.feedCardInfos) .catch(self.catchClosure), .just(.updateIsRefreshing(false)) @@ -131,7 +139,7 @@ class ProfileViewReactor: Reactor { .just(.updateIsRefreshing(true)), .just(.profile(profileInfo)) .catch(self.catchClosure), - object.userUseCase.myCommentCards(lastId: nil) + object.fetchCardUseCase.writtenCommentCards(lastId: nil) .map(Mutation.commentCardInfos) .catch(self.catchClosure), .just(.updateIsRefreshing(false)) @@ -145,16 +153,16 @@ class ProfileViewReactor: Reactor { if cardType == .feed { - return self.userUseCase.feedCards(userId: userId, lastId: lastId) + return self.fetchCardUseCase.writtenFeedCards(userId: userId, lastId: lastId) .map(Mutation.moreFeedCardInfos) } else { - return self.userUseCase.myCommentCards(lastId: lastId) + return self.fetchCardUseCase.writtenCommentCards(lastId: lastId) .map(Mutation.moreCommentCardInfos) } case .updateProfile: - return self.userUseCase.profile(userId: self.userId) + return self.fetchUserInfoUseCase.userInfo(userId: self.userId) .map { profileInfo -> ProfileInfo in // 사용자 닉네임 업데이트 UserDefaults.standard.nickname = profileInfo.nickname @@ -167,9 +175,9 @@ class ProfileViewReactor: Reactor { if self.entranceType == .other, let userId = self.currentState.profileInfo?.userId { return .concat([ - self.userUseCase.profile(userId: userId) + self.fetchUserInfoUseCase.userInfo(userId: userId) .map(Mutation.profile), - self.userUseCase.feedCards(userId: userId, lastId: nil) + self.fetchCardUseCase.writtenFeedCards(userId: userId, lastId: nil) .map(Mutation.feedCardInfos) ]) } @@ -186,7 +194,7 @@ class ProfileViewReactor: Reactor { return .concat([ .just(.updateIsBlocked(nil)), - self.userUseCase.updateBlocked(id: userId, isBlocked: !isBlocked) + self.blockUserUseCase.updateBlocked(userId: userId, isBlocked: !isBlocked) .map(Mutation.updateIsBlocked) ]) case .follow: @@ -197,7 +205,7 @@ class ProfileViewReactor: Reactor { return .concat([ .just(.updateIsFollowing(nil)), - self.userUseCase.updateFollowing(userId: userId, isFollow: !isFollowing) + self.updateFollowUseCase.updateFollowing(userId: userId, isFollow: !isFollowing) .map(Mutation.updateIsFollowing) ]) } @@ -242,7 +250,7 @@ extension ProfileViewReactor { return prevDisplayState.cardType == currDisplayState.cardType && prevDisplayState.profileInfo == currDisplayState.profileInfo && prevDisplayState.feedCardInfos == currDisplayState.feedCardInfos && - prevDisplayState.commCardInfos == currDisplayState.commCardInfos + prevDisplayState.commentCardInfos == currDisplayState.commentCardInfos } } @@ -252,15 +260,22 @@ extension ProfileViewReactor { SettingsViewReactor(dependencies: self.dependencies) } - func reactorForUpdate(nickname: String, image profileImage: UIImage?) -> UpdateProfileViewReactor { - UpdateProfileViewReactor(dependencies: self.dependencies, nickname: nickname, image: profileImage) + func reactorForUpdate( + nickname: String, + image profileImage: UIImage?, + imageName profileImageName: String? + ) -> UpdateProfileViewReactor { + UpdateProfileViewReactor( + dependencies: self.dependencies, + nickname: nickname, + image: profileImage, + imageName: profileImageName + ) } func reactorForDetail(_ selectedId: String) -> DetailViewReactor { DetailViewReactor( dependencies: self.dependencies, - self.currentState.cardType, - type: .navi, with: selectedId ) } @@ -285,7 +300,6 @@ extension ProfileViewReactor { enum EntranceType { case my - case myWithNavi case other } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewControler.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewControler.swift index ed554acf..c57dcc8d 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewControler.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewControler.swift @@ -110,6 +110,7 @@ class AnnouncementViewController: BaseNavigationViewController, View { reactor.state.map(\.announcements) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, announcements in object.announcements = announcements @@ -173,7 +174,13 @@ extension AnnouncementViewController: UITableViewDelegate { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.tableView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewReactor.swift index a97b9180..362cfd8f 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/AnnouncementViewReactor.swift @@ -32,31 +32,31 @@ class AnnouncementViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let notificationUseCase: NotificationUseCase + private let fetchNoticeUseCase: FetchNoticeUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.notificationUseCase = dependencies.rootContainer.resolve(NotificationUseCase.self) + self.fetchNoticeUseCase = dependencies.rootContainer.resolve(FetchNoticeUseCase.self) } func mutate(action: Action) -> Observable { switch action { case .landing: - return self.notificationUseCase.notices(lastId: nil, size: 10, requestType: .settings) + return self.fetchNoticeUseCase.notices(lastId: nil, size: 10, requestType: .settings) .map(Mutation.announcements) case .refresh: return .concat([ .just(.updateIsRefreshing(true)), - self.notificationUseCase.notices(lastId: nil, size: 10, requestType: .settings) + self.fetchNoticeUseCase.notices(lastId: nil, size: 10, requestType: .settings) .map(Mutation.announcements) - .catch { _ in .just(.updateIsRefreshing(false)) }, + .catchAndReturn(.updateIsRefreshing(false)), .just(.updateIsRefreshing(false)) ]) case let .more(lastId): - return self.notificationUseCase.notices(lastId: lastId, size: 10, requestType: .settings) + return self.fetchNoticeUseCase.notices(lastId: lastId, size: 10, requestType: .settings) .map(Mutation.more) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/Cells/AnnouncementViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/Cells/AnnouncementViewCell.swift index 90119d0c..5cdae115 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/Cells/AnnouncementViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Announcement/Cells/AnnouncementViewCell.swift @@ -18,7 +18,7 @@ class AnnouncementViewCell: UITableViewCell { private let titleLabel = UILabel().then { $0.textColor = .som.v2.black - $0.typography = .som.v2.subtitle3 + $0.typography = .som.v2.subtitle3.withAlignment(.left) $0.numberOfLines = 0 $0.lineBreakMode = .byTruncatingTail $0.lineBreakStrategy = .hangulWordPriority diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewController.swift index 12e73648..727438a9 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewController.swift @@ -78,11 +78,11 @@ class BlockUsersViewController: BaseNavigationViewController, View { cell.setModel(blockUserInfo) - cell.profileBackgroundButton.rx.throttleTap + cell.profileBackgroundButton.rx.throttleTap(.seconds(3)) .subscribe(with: self) { object, _ in let profileViewController = ProfileViewController() profileViewController.reactor = reactor.reactorForProfile(blockUserInfo.userId) - object.navigationPush(profileViewController, animated: true, bottomBarHidden: true) + object.navigationPush(profileViewController, animated: true) } .disposed(by: cell.disposeBag) @@ -285,7 +285,13 @@ extension BlockUsersViewController: UITableViewDelegate { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.tableView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewReactor.swift index f3eb6062..b221652e 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/BlockUsers/BlockUsersViewReactor.swift @@ -36,36 +36,36 @@ class BlockUsersViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let settingsUseCase: SettingsUseCase - private let userUseCase: UserUseCase + private let fetchBlockUserUseCase: FetchBlockUserUseCase + private let blockUserUseCase: BlockUserUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.fetchBlockUserUseCase = dependencies.rootContainer.resolve(FetchBlockUserUseCase.self) + self.blockUserUseCase = dependencies.rootContainer.resolve(BlockUserUseCase.self) } func mutate(action: Action) -> Observable { switch action { case .landing: - return self.settingsUseCase.blockUsers(lastId: nil) + return self.fetchBlockUserUseCase.blockUsers(lastId: nil) .map(Mutation.blockUserInfos) case .refresh: return .concat([ .just(.updateIsRefreshing(true)), - self.settingsUseCase.blockUsers(lastId: nil) + self.fetchBlockUserUseCase.blockUsers(lastId: nil) .map(Mutation.blockUserInfos), .just(.updateIsRefreshing(false)) ]) case let .moreFind(lastId): - return self.settingsUseCase.blockUsers(lastId: lastId) + return self.fetchBlockUserUseCase.blockUsers(lastId: lastId) .map(Mutation.more) case let .cancelBlock(userId): - return self.userUseCase.updateBlocked(id: userId, isBlocked: false) + return self.blockUserUseCase.updateBlocked(userId: userId, isBlocked: false) .map(Mutation.updateIsCanceled) } } @@ -91,8 +91,4 @@ extension BlockUsersViewReactor { func reactorForProfile(_ userId: String) -> ProfileViewReactor { ProfileViewReactor(dependencies: self.dependencies, type: .other, with: userId) } - - // func reactorForMainTabBar() -> MainTabBarReactor { - // MainTabBarReactor(provider: self.provider) - // } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewController.swift index fbb00deb..6281c15e 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewController.swift @@ -208,21 +208,23 @@ class EnterMemberTransferViewController: BaseNavigationViewController, View { let isSuccess = reactor.state.map(\.isSuccess).filterNil().share() isSuccess .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in guard let window = object.view.window else { return } object.showSuccessDialog { - let onboardingViewController = OnboardingViewController() - onboardingViewController.reactor = reactor.reactorForOnborading() - onboardingViewController.modalTransitionStyle = .crossDissolve - window.rootViewController = UINavigationController(rootViewController: onboardingViewController) + let launchScreenViewController = LaunchScreenViewController() + launchScreenViewController.reactor = reactor.reactorForLaunchScreen() + launchScreenViewController.modalTransitionStyle = .crossDissolve + window.rootViewController = launchScreenViewController } } .disposed(by: self.disposeBag) isSuccess .filter { $0 == false } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.showErrorDialog() } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewReactor.swift index 6a65a48f..bbdc0c01 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Enter/EnterMemberTransferViewReactor.swift @@ -33,12 +33,12 @@ class EnterMemberTransferViewReactor: Reactor { private let dependencies: AppDIContainerable private let authUseCase: AuthUseCase - private let settingsUseCase: SettingsUseCase + private let transferAccountUseCase: TransferAccountUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies self.authUseCase = dependencies.rootContainer.resolve(AuthUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.transferAccountUseCase = dependencies.rootContainer.resolve(TransferAccountUseCase.self) } func mutate(action: Action) -> Observable { @@ -52,12 +52,16 @@ class EnterMemberTransferViewReactor: Reactor { .flatMapLatest { object, encryptedDeviceId -> Observable in if let encryptedDeviceId = encryptedDeviceId { - return object.settingsUseCase.enter(code: transferCode, encryptedDeviceId: encryptedDeviceId) + return object.transferAccountUseCase.enter( + code: transferCode, + encryptedDeviceId: encryptedDeviceId + ) .flatMapLatest { isSuccess -> Observable in if isSuccess { object.authUseCase.initializeAuthInfo() } return .just(.enterTransferCode(isSuccess)) } + .catchAndReturn(.enterTransferCode(false)) } else { return .just(.enterTransferCode(false)) } @@ -78,7 +82,7 @@ class EnterMemberTransferViewReactor: Reactor { extension EnterMemberTransferViewReactor { - func reactorForOnborading() -> OnboardingViewReactor { - OnboardingViewReactor(dependencies: self.dependencies) + func reactorForLaunchScreen() -> LaunchScreenViewReactor { + LaunchScreenViewReactor(dependencies: self.dependencies) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewController.swift index dede243a..d082cc19 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewController.swift @@ -150,6 +150,7 @@ class IssueMemberTransferViewController: BaseNavigationViewController, View { // State reactor.state.map(\.isProcessing) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self.loadingIndicatorView) { loadingIndicatorView, isLoading in if isLoading { loadingIndicatorView.startAnimating() @@ -162,11 +163,13 @@ class IssueMemberTransferViewController: BaseNavigationViewController, View { let trnsferCodeInfo = reactor.state.map(\.trnsferCodeInfo).filterNil().distinctUntilChanged().share() trnsferCodeInfo .map(\.code) + .observe(on: MainScheduler.asyncInstance) .bind(to: self.transferCodeLabel.rx.text) .disposed(by: self.disposeBag) trnsferCodeInfo .map(\.expiredAt) + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, expiredAt in object.subscribePungTime(expiredAt) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewReactor.swift index 0c5bc4d0..0621b7c8 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/MemberTransfer/Issue/IssueMemberTransferViewReactor.swift @@ -31,26 +31,26 @@ class IssueMemberTransferViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let settingsUseCase: SettingsUseCase + private let transferAccountUseCase: TransferAccountUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.transferAccountUseCase = dependencies.rootContainer.resolve(TransferAccountUseCase.self) } func mutate(action: Action) -> Observable { switch action { case .landing: - return self.settingsUseCase.issue() + return self.transferAccountUseCase.issue() .map(Mutation.updateTransferInfo) case .updateTransferCode: return .concat([ .just(.updateIsProcessing(true)), - self.settingsUseCase.update() + self.transferAccountUseCase.update() .map(Mutation.updateTransferInfo) - .catch(self.catchClosure) + .catchAndReturn(.updateIsProcessing(false)) .delay(.milliseconds(1000), scheduler: MainScheduler.instance), .just(.updateIsProcessing(false)) ]) @@ -68,14 +68,3 @@ class IssueMemberTransferViewReactor: Reactor { return state } } - -extension IssueMemberTransferViewReactor { - - var catchClosure: ((Error) throws -> Observable ) { - return { _ in - .concat([ - .just(.updateIsProcessing(false)) - ]) - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewController.swift index 287bc736..d797c7ca 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewController.swift @@ -141,12 +141,12 @@ class ResignViewController: BaseNavigationViewController, View { let boundsHeight = self.scrollView.bounds.height - height let bottomOffset = CGPoint(x: 0, y: contentHeight - boundsHeight + 10) // 키보드 및 스크롤 애니메이션 동기화를 위해 `UIView.animate` 사용 - UIView.animate(withDuration: 0.25) { - self.view.layoutIfNeeded() + UIView.animate(withDuration: 0.25) { [weak self] in + self?.view.layoutIfNeeded() // 스크롤이 필요할 때만 적용 if bottomOffset.y > 0 { - self.scrollView.setContentOffset(bottomOffset, animated: false) + self?.scrollView.setContentOffset(bottomOffset, animated: false) } } } @@ -175,9 +175,10 @@ class ResignViewController: BaseNavigationViewController, View { // State reactor.state.map(\.isSuccess) - .filterNil() .distinctUntilChanged() + .filterNil() .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in guard let window = object.view.window else { return } @@ -194,8 +195,9 @@ class ResignViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) reactor.state.map(\.reason) - .filterNil() .distinctUntilChanged() + .filterNil() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, reason in let items = object.container.arrangedSubviews.compactMap { $0 as? SOMButton } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewReactor.swift index 12ed1bc8..36aa9725 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/Resign/ResignViewReactor.swift @@ -55,13 +55,15 @@ class ResignViewReactor: Reactor { (self.currentState.otherReason ?? reason.message) : reason.message ) - .map { isSuccess in + .withUnretained(self) + .flatMapLatest { object, isSuccess -> Observable in // 사용자 닉네임 제거 UserDefaults.standard.nickname = nil + // 인증 토큰 제거 + object.authUseCase.initializeAuthInfo() - return isSuccess + return .just(.updateIsSuccess(isSuccess)) } - .map(Mutation.updateIsSuccess) case let .updateReason(reason): return .just(.updateReason(reason)) diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewController.swift index 515df291..496d5ed5 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewController.swift @@ -30,7 +30,7 @@ class SettingsViewController: BaseNavigationViewController, View { static let announcementTitle: String = "공지사항" static let inquiryTitle: String = "문의하기" - static let acceptTermsTitle: String = "이용약관 및 개인정보 처리 방침" + static let acceptTermsTitle: String = "약관 및 개인정보 처리 동의" static let appVersionTitle: String = "최신버전 업데이트" static let latestVersionTitle: String = "최신버전 : " @@ -187,15 +187,6 @@ class SettingsViewController: BaseNavigationViewController, View { } } - override func bind() { - - self.navigationBar.backButton.rx.tap - .subscribe(with: self) { object, _ in - object.navigationPop(bottomBarHidden: false) - } - .disposed(by: self.disposeBag) - } - // MARK: ReactorKit - bind @@ -215,38 +206,43 @@ class SettingsViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) self.issueUserTransferCodeCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in let issueMemberTransferViewController = IssueMemberTransferViewController() issueMemberTransferViewController.reactor = reactor.reactorForTransferIssue() - object.navigationPush(issueMemberTransferViewController, animated: true, bottomBarHidden: true) + object.navigationPush(issueMemberTransferViewController, animated: true) } .disposed(by: self.disposeBag) self.enterUserTransferCodeCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in let enterMemberTransferViewController = EnterMemberTransferViewController() enterMemberTransferViewController.reactor = reactor.reactorForTransferEnter() - object.navigationPush(enterMemberTransferViewController, animated: true, bottomBarHidden: true) + object.navigationPush(enterMemberTransferViewController, animated: true) } .disposed(by: self.disposeBag) self.blockUsersCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in let blockUsersViewController = BlockUsersViewController() blockUsersViewController.reactor = reactor.reactorForBlock() - object.navigationPush(blockUsersViewController, animated: true, bottomBarHidden: true) + object.navigationPush(blockUsersViewController, animated: true) } .disposed(by: self.disposeBag) self.announcementCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in let announcementViewController = AnnouncementViewController() announcementViewController.reactor = reactor.reactorForAnnouncement() - object.navigationPush(announcementViewController, animated: true, bottomBarHidden: true) + object.navigationPush(announcementViewController, animated: true) } .disposed(by: self.disposeBag) self.inquiryCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(onNext: { _ in let subject = Text.inquiryMailTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" @@ -267,14 +263,16 @@ class SettingsViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) self.acceptTermsCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in let rermsOfServiceViewController = TermsOfServiceViewController() - object.navigationPush(rermsOfServiceViewController, animated: true, bottomBarHidden: true) + object.navigationPush(rermsOfServiceViewController, animated: true) } .disposed(by: self.disposeBag) let version = reactor.state.map(\.version).filterNil().distinctUntilChanged().share() self.appVersionCellView.rx.didSelect + .throttle(.seconds(1), scheduler: MainScheduler.instance) .withLatestFrom(version) .subscribe(with: self) { object, version in if version.mustUpdate { @@ -296,13 +294,13 @@ class SettingsViewController: BaseNavigationViewController, View { var wrapper: SwiftEntryKitViewWrapper = bottomFloatView.sek wrapper.entryName = Text.bottomToastEntryName - // TODO: 임시, 하단 네비바 높이를 34로 가정 후 사용 wrapper.showBottomToast(verticalOffset: 34 + 8, displayDuration: 4) } } .disposed(by: self.disposeBag) self.resignCellView.rx.didSelect + .throttle(.seconds(1), scheduler: MainScheduler.instance) .map { _ in Reactor.Action.rejoinableDate } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -310,6 +308,7 @@ class SettingsViewController: BaseNavigationViewController, View { // State reactor.state.map(\.banEndAt) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, banEndAt in object.postingBlockedBackgroundView.isHidden = (banEndAt == nil) object.postingBlockedMessageLabel.text = Text.postingBlockedLeadingGuideMessage + @@ -320,6 +319,7 @@ class SettingsViewController: BaseNavigationViewController, View { reactor.state.map(\.rejoinableDate) .filterNil() + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, rejoinableDate in object.showResignDialog(rejoinableDate: rejoinableDate) } @@ -327,10 +327,12 @@ class SettingsViewController: BaseNavigationViewController, View { reactor.state.map(\.notificationStatus) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .bind(to: self.notificationSettingCellView.toggleSwitch.rx.isOn) .disposed(by: self.disposeBag) version + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, version in object.appVersionCellView.setLatestVersion(Text.latestVersionTitle + version.latestVersion) } @@ -338,6 +340,7 @@ class SettingsViewController: BaseNavigationViewController, View { reactor.state.map(\.shouldHideTransfer) .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, shouldHide in object.issueUserTransferCodeCellView.isHidden = shouldHide object.enterUserTransferCodeCellView.isHidden = shouldHide @@ -374,8 +377,7 @@ extension SettingsViewController { resignViewController.reactor = reactor.reactorForResign() self.navigationPush( resignViewController, - animated: true, - bottomBarHidden: true + animated: true ) { _ in reactor.action.onNext(.resetState) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewReactor.swift index f13a8908..ef096cc4 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/SettingsViewReactor.swift @@ -41,21 +41,21 @@ class SettingsViewReactor: Reactor { private let dependencies: AppDIContainerable private let appVersionUseCase: AppVersionUseCase private let authUseCase: AuthUseCase - private let userUseCase: UserUseCase - private let settingsUseCase: SettingsUseCase + private let validateUserUseCase: ValidateUserUseCase + private let updateNotifyUseCase: UpdateNotifyUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies self.appVersionUseCase = dependencies.rootContainer.resolve(AppVersionUseCase.self) self.authUseCase = dependencies.rootContainer.resolve(AuthUseCase.self) - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.validateUserUseCase = dependencies.rootContainer.resolve(ValidateUserUseCase.self) + self.updateNotifyUseCase = dependencies.rootContainer.resolve(UpdateNotifyUseCase.self) self.initialState = .init( tokens: self.authUseCase.tokens(), banEndAt: nil, version: nil, - notificationStatus: self.settingsUseCase.notificationStatus(), + notificationStatus: self.updateNotifyUseCase.notificationStatus(), shouldHideTransfer: UserDefaults.standard.bool(forKey: "AppFlag") ) } @@ -67,20 +67,20 @@ class SettingsViewReactor: Reactor { return .concat([ self.appVersionUseCase.version() .map(Mutation.updateVersion), - self.userUseCase.postingPermission() + self.validateUserUseCase.postingPermission() .map(\.expiredAt) .map(Mutation.updateBanEndAt), - self.userUseCase.updateNotify(isAllowNotify: self.initialState.notificationStatus) + self.updateNotifyUseCase.updateNotify(isAllowNotify: self.initialState.notificationStatus) .map(Mutation.updateNotificationStatus) ]) case let .updateNotificationStatus(state): - return self.userUseCase.updateNotify(isAllowNotify: state) + return self.updateNotifyUseCase.updateNotify(isAllowNotify: state) .map { _ in state } .map(Mutation.updateNotificationStatus) case .rejoinableDate: - return self.settingsUseCase.rejoinableDate() + return self.validateUserUseCase.iswithdrawn() .map(Mutation.rejoinableDate) case .resetState: diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/termsOfService/TermsOfServiceViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/termsOfService/TermsOfServiceViewController.swift index 7ecdd128..46a0e12a 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/termsOfService/TermsOfServiceViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/termsOfService/TermsOfServiceViewController.swift @@ -21,6 +21,10 @@ class TermsOfServiceViewController: BaseNavigationViewController { static let privacyPolicyTitle: String = "개인정보처리방침" static let termsOfServiceTitle: String = "서비스 이용약관" static let termsOfLocationInfoTitle: String = "위치정보 이용약관" + + static let privacyPolicyURLString: String = "https://adjoining-guanaco-d0a.notion.site/26b2142ccaa38059a1dbf3e6b6b6b4e6?pvs=74" + static let termsOfServiceURLString: String = "https://adjoining-guanaco-d0a.notion.site/26b2142ccaa38076b491df099cd7b559" + static let termsOfLocationInfoURLString: String = "https://adjoining-guanaco-d0a.notion.site/26b2142ccaa380f1bfafe99f5f8a10f1?pvs=74" } private let scrollView = UIScrollView().then { @@ -83,8 +87,9 @@ class TermsOfServiceViewController: BaseNavigationViewController { super.bind() self.privacyPolicyCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in - if let url = URL(string: "https://mewing-space-6d3.notion.site/3f92380d536a4b569921d2809ed147ef?pvs=4") { + if let url = URL(string: Text.privacyPolicyURLString) { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } @@ -93,8 +98,9 @@ class TermsOfServiceViewController: BaseNavigationViewController { .disposed(by: self.disposeBag) self.termsOfServiceCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in - if let url = URL(string: "https://mewing-space-6d3.notion.site/45d151f68ba74b23b24483ad8b2662b4?pvs=4") { + if let url = URL(string: Text.termsOfServiceURLString) { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } @@ -103,8 +109,9 @@ class TermsOfServiceViewController: BaseNavigationViewController { .disposed(by: self.disposeBag) self.termsOfLocationInfoCellView.rx.didSelect + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, _ in - if let url = URL(string: "https://mewing-space-6d3.notion.site/44e378c9d11d45159859492434b6b128?pvs=4") { + if let url = URL(string: Text.termsOfLocationInfoURLString) { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewController.swift index 6f9b5475..ad7d748c 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewController.swift @@ -212,7 +212,7 @@ class UpdateProfileViewController: BaseNavigationViewController, View { self.profileImageView.rx.tapGesture().when(.ended).map { _ in }, self.cameraButton.rx.tap.asObservable() ) - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let status = PHPhotoLibrary.authorizationStatus(for: .readWrite) @@ -289,10 +289,10 @@ class UpdateProfileViewController: BaseNavigationViewController, View { reactor.state.map(\.isUpdatedSuccess) .distinctUntilChanged() .filter { $0 } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in - object.navigationPop(bottomBarHidden: false) { - NotificationCenter.default.post(name: .reloadProfileData, object: nil, userInfo: nil) - } + NotificationCenter.default.post(name: .reloadProfileData, object: nil, userInfo: nil) + object.navigationPop() } .disposed(by: self.disposeBag) @@ -308,28 +308,11 @@ class UpdateProfileViewController: BaseNavigationViewController, View { reactor.state.map(\.hasErrors) .distinctUntilChanged() .filter { $0 == true } - .observe(on: MainScheduler.asyncInstance) - .subscribe(onNext: { _ in - - let actions: [SOMDialogAction] = [ - .init( - title: Text.inappositeDialogConfirmButtonTitle, - style: .primary, - action: { - UIApplication.topViewController?.dismiss(animated: true) { - reactor.action.onNext(.setDefaultImage) - } - } - ) - ] + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, _ in - SOMDialogViewController.show( - title: Text.inappositeDialogTitle, - message: Text.inappositeDialogMessage, - textAlignment: .left, - actions: actions - ) - }) + object.showInappositeDialog(reactor) + } .disposed(by: self.disposeBag) reactor.state.map(\.errorMessage) @@ -358,14 +341,15 @@ private extension UpdateProfileViewController { title: Text.settingActionTitle, style: .primary, action: { - let application = UIApplication.shared - let openSettingsURLString: String = UIApplication.openSettingsURLString - if let settingsURL = URL(string: openSettingsURLString), - application.canOpenURL(settingsURL) { - application.open(settingsURL) + UIApplication.topViewController?.dismiss(animated: true) { + + let application = UIApplication.shared + let openSettingsURLString: String = UIApplication.openSettingsURLString + if let settingsURL = URL(string: openSettingsURLString), + application.canOpenURL(settingsURL) { + application.open(settingsURL) + } } - - UIApplication.topViewController?.dismiss(animated: true) } ) @@ -376,6 +360,28 @@ private extension UpdateProfileViewController { ) } + func showInappositeDialog(_ reactor: UpdateProfileViewReactor) { + + let actions: [SOMDialogAction] = [ + .init( + title: Text.inappositeDialogConfirmButtonTitle, + style: .primary, + action: { + UIApplication.topViewController?.dismiss(animated: true) { + reactor.action.onNext(.setDefaultImage) + } + } + ) + ] + + SOMDialogViewController.show( + title: Text.inappositeDialogTitle, + message: Text.inappositeDialogMessage, + textAlignment: .left, + actions: actions + ) + } + func showPicker(for screen: YPPickerScreen) { var config = YPImagePickerConfiguration() @@ -414,8 +420,8 @@ private extension UpdateProfileViewController { picker?.dismiss(animated: true, completion: nil) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self.present(picker, animated: true, completion: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in + self?.present(picker, animated: true, completion: nil) } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewReactor.swift index c14edaaa..5bd968db 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/UpdateProfile/UpdateProfileViewReactor.swift @@ -35,35 +35,40 @@ class UpdateProfileViewReactor: Reactor { } struct State { - var profileImage: UIImage? - var profileImageName: String? - var isValid: Bool - var isUpdatedSuccess: Bool - var isProcessing: Bool - var hasErrors: Bool? - var errorMessage: String? + fileprivate(set) var profileImage: UIImage? + fileprivate(set) var profileImageName: String? + fileprivate(set) var isValid: Bool + fileprivate(set) var isUpdatedSuccess: Bool + fileprivate(set) var isProcessing: Bool + fileprivate(set) var hasErrors: Bool? + fileprivate(set) var errorMessage: String? } var initialState: State - private var imageName: String? - private let dependencies: AppDIContainerable - private let userUseCase: UserUseCase + private let validateNicknameUseCase: ValidateNicknameUseCase + private let uploadUserImageUseCase: UploadUserImageUseCase + private let updateUserInfoUseCase: UpdateUserInfoUseCase let nickname: String init( dependencies: AppDIContainerable, nickname: String, - image profileImage: UIImage? + image profileImage: UIImage?, + imageName profileImageName: String? ) { self.dependencies = dependencies - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.validateNicknameUseCase = dependencies.rootContainer.resolve(ValidateNicknameUseCase.self) + self.uploadUserImageUseCase = dependencies.rootContainer.resolve(UploadUserImageUseCase.self) + self.updateUserInfoUseCase = dependencies.rootContainer.resolve(UpdateUserInfoUseCase.self) + self.nickname = nickname self.initialState = .init( profileImage: profileImage, + profileImageName: profileImageName, isValid: false, isUpdatedSuccess: false, isProcessing: false, @@ -105,7 +110,7 @@ class UpdateProfileViewReactor: Reactor { return .concat([ .just(.updateErrorMessage(nil)), - self.userUseCase.isNicknameValid(nickname: nickname) + self.validateNicknameUseCase.checkValidation(nickname: nickname) .withUnretained(self) .flatMapLatest { object, isValid -> Observable in @@ -122,7 +127,7 @@ class UpdateProfileViewReactor: Reactor { let updatedNickname = trimedNickname == self.nickname ? nil : trimedNickname return .concat([ .just(.updateErrors(false)), - self.userUseCase.updateMyProfile( + self.updateUserInfoUseCase.updateUserInfo( nickname: updatedNickname, imageName: self.currentState.profileImageName ) @@ -163,7 +168,7 @@ extension UpdateProfileViewReactor { if let imageData = image.jpegData(compressionQuality: 0.5), let url = URL(string: presignedInfo.imgUrl) { - return object.userUseCase.uploadImage(imageData, with: url) + return object.uploadUserImageUseCase.uploadToS3(imageData, with: url) .flatMapLatest { isSuccess -> Observable in let image = isSuccess ? image : nil @@ -180,7 +185,7 @@ extension UpdateProfileViewReactor { private func presignedURL() -> Observable { - return self.userUseCase.presignedURL() + return self.uploadUserImageUseCase.presignedURL() } private var catchClosure: ((Error) throws -> Observable ) { diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Cells/FavoriteTagViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Cells/FavoriteTagViewCell.swift index 3c5c8607..851906e6 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Cells/FavoriteTagViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Cells/FavoriteTagViewCell.swift @@ -124,7 +124,7 @@ class FavoriteTagViewCell: UICollectionViewCell { } itemContainer.rx.tapGesture() - .throttle(.seconds(3), scheduler: MainScheduler.instance) + .throttle(.seconds(2), scheduler: MainScheduler.instance) .when(.recognized) .subscribe(with: self) { object, gesture in guard itemContainer.isTappedDirectly(gesture: gesture) else { return } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Cells/PopularTagViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Cells/PopularTagViewCell.swift index cff2c553..b79f0836 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Cells/PopularTagViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Cells/PopularTagViewCell.swift @@ -72,34 +72,6 @@ class PopularTagViewCell: UICollectionViewCell { self.countLabel.text = nil } - // Set highlighted color -// override func touchesBegan(_ touches: Set, with event: UIEvent?) { -// -// guard let touch = touches.first else { return } -// -// let location = touch.location(in: self.container) -// if self.container.frame.contains(location) { -// -// self.updateColors(true) -// } -// -// super.touchesBegan(touches, with: event) -// } -// -// override func touchesEnded(_ touches: Set, with event: UIEvent?) { -// -// self.updateColors(false) -// -// super.touchesEnded(touches, with: event) -// } -// -// override func touchesCancelled(_ touches: Set, with event: UIEvent?) { -// -// self.updateColors(false) -// -// super.touchesCancelled(touches, with: event) -// } - // MARK: Private func diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift index bf6efb02..4725e476 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/Cells/TagCollectCardsView.swift @@ -224,7 +224,13 @@ extension TagCollectCardsView: UICollectionViewDelegateFlowLayout { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.collectionView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewController.swift index c95ef374..d3fa9cbc 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewController.swift @@ -26,6 +26,9 @@ class TagCollectViewController: BaseNavigationViewController, View { static let bottomToastEntryNameWithAction: String = "bottomToastEntryNameWithAction" static let failedToastMessage: String = "네트워크 확인 후 재시도해주세요." static let failToastActionTitle: String = "재시도" + + static let bottomToastEntryNameWithoutAction: String = "bottomToastEntryNameWithoutAction" + static let addAdditionalLimitedFloatMessage: String = "관심 태그는 9개까지 추가할 수 있어요" } enum Section: Int, CaseIterable { @@ -76,13 +79,14 @@ class TagCollectViewController: BaseNavigationViewController, View { /// 뒤로가기로 TagViewController를 표시할 때, 관심 태그만 리로드 self.navigationBar.backButton.rx.throttleTap .subscribe(with: self) { object, _ in - object.navigationPop(animated: true, bottomBarHidden: true) { + if object.reactor?.initialState.isFavorite != object.reactor?.currentState.isFavorite { NotificationCenter.default.post( name: .reloadFavoriteTagData, object: nil, userInfo: nil ) } + object.navigationPop() } .disposed(by: self.disposeBag) } @@ -98,7 +102,7 @@ class TagCollectViewController: BaseNavigationViewController, View { .subscribe(with: self) { object, model in let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail(model.id) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + object.navigationPush(detailViewController, animated: true) } .disposed(by: self.disposeBag) @@ -180,8 +184,22 @@ class TagCollectViewController: BaseNavigationViewController, View { let bottomToastView = SOMBottomToastView(title: Text.failedToastMessage, actions: actions) var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek - wrapper.entryName = Text.bottomToastEntryName - wrapper.showBottomToast(verticalOffset: 34 + 54 + 8) + wrapper.entryName = Text.bottomToastEntryNameWithAction + wrapper.showBottomToast(verticalOffset: 34 + 8) + } + .disposed(by: self.disposeBag) + + reactor.state.map(\.hasErrors) + .distinctUntilChanged() + .filterNil() + .filter { $0 } + .subscribe(with: self) { object, _ in + + let bottomToastView = SOMBottomToastView(title: Text.addAdditionalLimitedFloatMessage, actions: nil) + + var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek + wrapper.entryName = Text.bottomToastEntryNameWithoutAction + wrapper.showBottomToast(verticalOffset: 34 + 8) } .disposed(by: self.disposeBag) diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewReactor.swift index aad971a6..2c952d31 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Collect/TagCollectViewReactor.swift @@ -22,6 +22,7 @@ class TagCollectViewReactor: Reactor { case updateIsFavorite(Bool) case updateIsUpdate(Bool?) case updateIsRefreshing(Bool) + case updateHasErrors(Bool?) } struct State { @@ -29,19 +30,22 @@ class TagCollectViewReactor: Reactor { fileprivate(set) var isFavorite: Bool fileprivate(set) var isUpdated: Bool? fileprivate(set) var isRefreshing: Bool + fileprivate(set) var hasErrors: Bool? } var initialState: State private let dependencies: AppDIContainerable - private let tagUseCase: TagUseCase + private let fetchCardUseCase: FetchCardUseCase + private let updateTagFavoriteUseCase: UpdateTagFavoriteUseCase private let id: String let title: String init(dependencies: AppDIContainerable, with id: String, title: String, isFavorite: Bool) { self.dependencies = dependencies - self.tagUseCase = dependencies.rootContainer.resolve(TagUseCase.self) + self.fetchCardUseCase = dependencies.rootContainer.resolve(FetchCardUseCase.self) + self.updateTagFavoriteUseCase = dependencies.rootContainer.resolve(UpdateTagFavoriteUseCase.self) self.id = id self.title = title @@ -50,7 +54,8 @@ class TagCollectViewReactor: Reactor { tagCardInfos: [], isFavorite: isFavorite, isUpdated: nil, - isRefreshing: false + isRefreshing: false, + hasErrors: nil ) } @@ -58,7 +63,7 @@ class TagCollectViewReactor: Reactor { switch action { case .landing: - return self.tagUseCase.tagCards(tagId: self.id, lastId: nil) + return self.fetchCardUseCase.cardsWithTag(tagId: self.id, lastId: nil) .flatMapLatest { tagCardsInfo -> Observable in let isFavorite = tagCardsInfo.cardInfos.isEmpty ? @@ -73,7 +78,7 @@ class TagCollectViewReactor: Reactor { return .concat([ .just(.updateIsRefreshing(true)), - self.tagUseCase.tagCards(tagId: self.id, lastId: nil) + self.fetchCardUseCase.cardsWithTag(tagId: self.id, lastId: nil) .flatMapLatest { tagCardsInfo -> Observable in let isFavorite = tagCardsInfo.cardInfos.isEmpty ? @@ -88,14 +93,15 @@ class TagCollectViewReactor: Reactor { ]) case let .more(lastId): - return self.tagUseCase.tagCards(tagId: self.id, lastId: lastId) + return self.fetchCardUseCase.cardsWithTag(tagId: self.id, lastId: lastId) .map(\.cardInfos) .map(Mutation.moreFind) case let .updateIsFavorite(isFavorite): return .concat([ .just(.updateIsUpdate(nil)), - self.tagUseCase.updateFavorite(tagId: self.id, isFavorite: !isFavorite) + .just(.updateHasErrors(nil)), + self.updateTagFavoriteUseCase.updateFavorite(tagId: self.id, isFavorite: !isFavorite) .flatMapLatest { isUpdated -> Observable in let isFavorite = isUpdated ? !isFavorite : isFavorite @@ -104,6 +110,7 @@ class TagCollectViewReactor: Reactor { .just(.updateIsUpdate(isUpdated)) ]) } + .catch(self.catchClosure) ]) } } @@ -121,14 +128,33 @@ class TagCollectViewReactor: Reactor { newState.isUpdated = isUpdated case let .updateIsRefreshing(isRefreshing): newState.isRefreshing = isRefreshing + case let .updateHasErrors(hasErrors): + newState.hasErrors = hasErrors } return newState } } +private extension TagCollectViewReactor { + + var catchClosure: ((Error) throws -> Observable) { + return { error in + + let nsError = error as NSError + + if case 400 = nsError.code { + + return .just(.updateHasErrors(true)) + } + + return .just(.updateIsUpdate(false)) + } + } +} + extension TagCollectViewReactor { func reactorForDetail(_ selectedId: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, .feed, type: .navi, with: selectedId) + DetailViewReactor(dependencies: self.dependencies, with: selectedId) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift index 23211666..294f875f 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewController.swift @@ -26,6 +26,9 @@ class TagSearchCollectViewController: BaseNavigationViewController, View { static let bottomToastEntryNameWithAction: String = "bottomToastEntryNameWithAction" static let failedToastMessage: String = "네트워크 확인 후 재시도해주세요." static let failToastActionTitle: String = "재시도" + + static let bottomToastEntryNameWithoutAction: String = "bottomToastEntryNameWithoutAction" + static let addAdditionalLimitedFloatMessage: String = "관심 태그는 9개까지 추가할 수 있어요" } @@ -80,10 +83,11 @@ class TagSearchCollectViewController: BaseNavigationViewController, View { // 상세 화면 전환 self.tagCollectCardsView.cardDidTapped + .throttle(.seconds(3), scheduler: MainScheduler.instance) .subscribe(with: self) { object, model in let detailViewController = DetailViewController() detailViewController.reactor = reactor.reactorForDetail(with: model.id) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + object.navigationPush(detailViewController, animated: true) } .disposed(by: self.disposeBag) @@ -173,8 +177,22 @@ class TagSearchCollectViewController: BaseNavigationViewController, View { let bottomToastView = SOMBottomToastView(title: Text.failedToastMessage, actions: actions) var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek - wrapper.entryName = Text.bottomToastEntryName - wrapper.showBottomToast(verticalOffset: 34 + 54 + 8) + wrapper.entryName = Text.bottomToastEntryNameWithAction + wrapper.showBottomToast(verticalOffset: 34 + 8) + } + .disposed(by: self.disposeBag) + + reactor.state.map(\.hasErrors) + .distinctUntilChanged() + .filterNil() + .filter { $0 } + .subscribe(with: self) { object, _ in + + let bottomToastView = SOMBottomToastView(title: Text.addAdditionalLimitedFloatMessage, actions: nil) + + var wrapper: SwiftEntryKitViewWrapper = bottomToastView.sek + wrapper.entryName = Text.bottomToastEntryNameWithoutAction + wrapper.showBottomToast(verticalOffset: 34 + 8) } .disposed(by: self.disposeBag) diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift index a49ee6a8..e0de4618 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Search+Collect/TagSearchCollectViewReactor.swift @@ -22,6 +22,7 @@ class TagSearchCollectViewReactor: Reactor { case updateIsUpdate(Bool?) case updateIsFavorite(Bool) case updateIsRefreshing(Bool) + case updateHasErrors(Bool?) } struct State { @@ -29,24 +30,30 @@ class TagSearchCollectViewReactor: Reactor { fileprivate(set) var isUpdated: Bool? fileprivate(set) var isFavorite: Bool fileprivate(set) var isRefreshing: Bool + fileprivate(set) var hasErrors: Bool? } var initialState: State = .init( tagCardInfos: [], isUpdated: nil, isFavorite: false, - isRefreshing: false + isRefreshing: false, + hasErrors: nil ) private let dependencies: AppDIContainerable - private let tagUseCase: TagUseCase + private let fetchCardUseCase: FetchCardUseCase + private let fetchTagUseCase: FetchTagUseCase + private let updateTagFavoriteUseCase: UpdateTagFavoriteUseCase private let tagId: String let title: String init(dependencies: AppDIContainerable, with tagId: String, title: String) { self.dependencies = dependencies - self.tagUseCase = dependencies.rootContainer.resolve(TagUseCase.self) + self.fetchCardUseCase = dependencies.rootContainer.resolve(FetchCardUseCase.self) + self.fetchTagUseCase = dependencies.rootContainer.resolve(FetchTagUseCase.self) + self.updateTagFavoriteUseCase = dependencies.rootContainer.resolve(UpdateTagFavoriteUseCase.self) self.tagId = tagId self.title = title @@ -56,53 +63,27 @@ class TagSearchCollectViewReactor: Reactor { switch action { case .landing: - return self.tagUseCase.tagCards(tagId: self.tagId, lastId: nil) - .withUnretained(self) - .flatMapLatest { object, tagCardsInfo -> Observable in - - if tagCardsInfo.cardInfos.isEmpty { - return object.tagUseCase.favorites() - .flatMapLatest { favoriteTagInfo -> Observable in - let isFavorite = favoriteTagInfo.contains( - FavoriteTagInfo(id: object.tagId, title: object.title) - ) - return .concat([ - .just(.tagCardInfos(tagCardsInfo.cardInfos)), - .just(.updateIsFavorite(isFavorite)) - ]) - } - } - - return .concat([ - .just(.tagCardInfos(tagCardsInfo.cardInfos)), - .just(.updateIsFavorite(tagCardsInfo.isFavorite)) - ]) - } + return self.cardsWithTag(with: nil) + .catchAndReturn(.tagCardInfos([])) case .refresh: return .concat([ .just(.updateIsRefreshing(true)), - self.tagUseCase.tagCards(tagId: self.tagId, lastId: nil) - .flatMapLatest { tagCardsInfo -> Observable in - - return .concat([ - .just(.tagCardInfos(tagCardsInfo.cardInfos)), - .just(.updateIsFavorite(tagCardsInfo.isFavorite)) - ]) - } + self.cardsWithTag(with: nil) .catchAndReturn(.tagCardInfos([])), .just(.updateIsRefreshing(false)) ]) case let .more(lastId): - return self.tagUseCase.tagCards(tagId: self.tagId, lastId: lastId) + return self.fetchCardUseCase.cardsWithTag(tagId: self.tagId, lastId: lastId) .map(\.cardInfos) .map(Mutation.moreFind) case let .updateIsFavorite(isFavorite): return .concat([ .just(.updateIsUpdate(nil)), - self.tagUseCase.updateFavorite(tagId: self.tagId, isFavorite: !isFavorite) + .just(.updateHasErrors(nil)), + self.updateTagFavoriteUseCase.updateFavorite(tagId: self.tagId, isFavorite: !isFavorite) .flatMapLatest { isUpdated -> Observable in let isFavorite = isUpdated ? !isFavorite : isFavorite @@ -111,6 +92,7 @@ class TagSearchCollectViewReactor: Reactor { .just(.updateIsUpdate(isUpdated)) ]) } + .catch(self.catchClosure) ]) } } @@ -128,14 +110,58 @@ class TagSearchCollectViewReactor: Reactor { newState.isFavorite = isFavorite case let .updateIsRefreshing(isRefreshing): newState.isRefreshing = isRefreshing + case let .updateHasErrors(hasErrors): + newState.hasErrors = hasErrors } return newState } } +private extension TagSearchCollectViewReactor { + + func cardsWithTag(with lastId: String?) -> Observable { + + return self.fetchCardUseCase.cardsWithTag(tagId: self.tagId, lastId: nil) + .withUnretained(self) + .flatMapLatest { object, tagCardsInfo -> Observable in + + if tagCardsInfo.cardInfos.isEmpty { + let tagInfo = FavoriteTagInfo(id: object.tagId, title: object.title) + return object.fetchTagUseCase.isFavorites(with: tagInfo) + .flatMapLatest { isFavorite -> Observable in + + return .concat([ + .just(.tagCardInfos(tagCardsInfo.cardInfos)), + .just(.updateIsFavorite(isFavorite)) + ]) + } + } + + return .concat([ + .just(.tagCardInfos(tagCardsInfo.cardInfos)), + .just(.updateIsFavorite(tagCardsInfo.isFavorite)) + ]) + } + } + + var catchClosure: ((Error) throws -> Observable) { + return { error in + + let nsError = error as NSError + + if case 400 = nsError.code { + + return .just(.updateHasErrors(true)) + } + + return .just(.updateIsUpdate(false)) + } + } +} + extension TagSearchCollectViewReactor { func reactorForDetail(with id: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, .feed, type: .navi, with: id) + DetailViewReactor(dependencies: self.dependencies, with: id) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift index 04dcdf1b..04f40d30 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewController.swift @@ -67,6 +67,7 @@ class TagSearchViewController: BaseNavigationViewController, View { func bind(reactor: TagSearchViewReactor) { + // 검색 화면 진입 시 포커스 self.rx.viewDidAppear .subscribe(with: self) { object, _ in object.searchTextFieldView.becomeFirstResponder() @@ -78,17 +79,17 @@ class TagSearchViewController: BaseNavigationViewController, View { self.navigationBar.backButton.rx.throttleTap .withLatestFrom(searchTerms) .map { $0 == nil } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, isNil in // 검색 결과가 없을 때만 if isNil { /// 뒤로가기로 TagViewController를 표시할 때, 관심 태그만 리로드 - object.navigationPop(animated: true, bottomBarHidden: false) { - NotificationCenter.default.post( - name: .reloadFavoriteTagData, - object: nil, - userInfo: nil - ) - } + NotificationCenter.default.post( + name: .reloadFavoriteTagData, + object: nil, + userInfo: nil + ) + object.navigationPop() } else { object.reactor?.action.onNext(.reset) object.searchTextFieldView.text = nil @@ -109,11 +110,10 @@ class TagSearchViewController: BaseNavigationViewController, View { with: model.id, title: model.name ) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - object.navigationPush( + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak object] in + object?.navigationPush( tagSearchCollectViewController, - animated: true, - bottomBarHidden: true + animated: true ) } } @@ -138,6 +138,7 @@ class TagSearchViewController: BaseNavigationViewController, View { // State searchTerms .filter { $0 == nil } + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in object.searchTermsView.isHidden = true } @@ -145,6 +146,7 @@ class TagSearchViewController: BaseNavigationViewController, View { searchTerms .filterNil() + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, searchTerms in object.searchTermsView.setModels(searchTerms) object.searchTermsView.isHidden = false diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift index 639cdc51..e1fd8d30 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/TagSearchViewReactor.swift @@ -12,33 +12,29 @@ class TagSearchViewReactor: Reactor { enum Action: Equatable { case reset case search(String) - case updateIsFavorite(String, Bool) } enum Mutation { case searchTerms([TagInfo]?) case updateIsUpdate(Bool?) - case updateIsFavorite(Bool) } struct State { fileprivate(set) var searchTerms: [TagInfo]? fileprivate(set) var isUpdated: Bool? - fileprivate(set) var isFavorite: Bool } var initialState: State = .init( searchTerms: nil, - isUpdated: nil, - isFavorite: false + isUpdated: nil ) private let dependencies: AppDIContainerable - private let tagUseCase: TagUseCase + private let fetchTagUseCase: FetchTagUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.tagUseCase = dependencies.rootContainer.resolve(TagUseCase.self) + self.fetchTagUseCase = dependencies.rootContainer.resolve(FetchTagUseCase.self) } func mutate(action: Action) -> Observable { @@ -48,22 +44,8 @@ class TagSearchViewReactor: Reactor { return .just(.searchTerms(nil)) case let .search(terms): - return self.tagUseCase.related(keyword: terms, size: 20) + return self.fetchTagUseCase.related(keyword: terms, size: 20) .map(Mutation.searchTerms) - case let .updateIsFavorite(tagId, isFavorite): - - return .concat([ - .just(.updateIsUpdate(nil)), - self.tagUseCase.updateFavorite(tagId: tagId, isFavorite: !isFavorite) - .flatMapLatest { isUpdated -> Observable in - - let isFavorite = isUpdated ? !isFavorite : isFavorite - return .concat([ - .just(.updateIsFavorite(isFavorite)), - .just(.updateIsUpdate(isUpdated)) - ]) - } - ]) } } @@ -74,8 +56,6 @@ class TagSearchViewReactor: Reactor { newState.searchTerms = searchTerms case let .updateIsUpdate(isUpdated): newState.isUpdated = isUpdated - case let .updateIsFavorite(isFavorite): - newState.isFavorite = isFavorite } return newState } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift index 8e45417c..673250e1 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Search/Views/SearchTextFieldView.swift @@ -72,7 +72,7 @@ class SearchTextFieldView: UIView { var text: String? { set { self.textField.text = newValue - self.textField.sendActions(for: .valueChanged) + self.textField.sendActions(for: .editingChanged) } get { return self.textField.text @@ -149,7 +149,7 @@ class SearchTextFieldView: UIView { private func clear() { self.clearButton.isHidden = true self.text = nil - self.textField.sendActions(for: .valueChanged) + self.textField.sendActions(for: .editingChanged) if self.textField.isFirstResponder == false { self.textField.becomeFirstResponder() } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/TagViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/TagViewController.swift index 12cf8849..74ea69e5 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/TagViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/TagViewController.swift @@ -48,7 +48,7 @@ class TagViewController: BaseNavigationViewController, View { $0.showsVerticalScrollIndicator = false $0.showsHorizontalScrollIndicator = false - $0.contentInset.bottom = 54 + 16 + $0.contentInsetAdjustmentBehavior = .never $0.refreshControl = SOMRefreshControl() @@ -125,7 +125,7 @@ class TagViewController: BaseNavigationViewController, View { super.viewDidLoad() // 제스처 뒤로가기를 위한 델리게이트 설정 - self.navigationController?.interactivePopGestureRecognizer?.delegate = self + self.parent?.navigationController?.interactivePopGestureRecognizer?.delegate = self NotificationCenter.default.addObserver( self, @@ -133,8 +133,6 @@ class TagViewController: BaseNavigationViewController, View { name: .reloadFavoriteTagData, object: nil ) - - self.favoriteTagHeaderView.title = (UserDefaults.standard.nickname ?? "") + Text.favoriteTagHeaderTitle } @@ -142,18 +140,11 @@ class TagViewController: BaseNavigationViewController, View { func bind(reactor: TagViewReactor) { - // tabBar 표시 - self.rx.viewDidAppear - .subscribe(with: self) { object, _ in - object.hidesBottomBarWhenPushed = false - } - .disposed(by: self.disposeBag) - self.searchViewButtonView.rx.didTap .subscribe(with: self) { object, _ in let tagSearchViewController = TagSearchViewController() tagSearchViewController.reactor = reactor.reactorForSearch() - object.navigationPush(tagSearchViewController, animated: true, bottomBarHidden: true) + object.parent?.navigationPush(tagSearchViewController, animated: true) } .disposed(by: self.disposeBag) @@ -165,12 +156,12 @@ class TagViewController: BaseNavigationViewController, View { title: model.text, isFavorite: model.isFavorite ) - object.navigationPush(tagCollectViewController, animated: true, bottomBarHidden: true) + object.parent?.navigationPush(tagCollectViewController, animated: true) } .disposed(by: self.disposeBag) self.popularTagsView.backgroundDidTap - .throttle(.seconds(3), scheduler: MainScheduler.instance) + .throttle(.seconds(2), scheduler: MainScheduler.instance) .subscribe(with: self) { object, model in let tagCollectViewController = TagCollectViewController() tagCollectViewController.reactor = reactor.reactorForCollect( @@ -178,7 +169,7 @@ class TagViewController: BaseNavigationViewController, View { title: model.name, isFavorite: reactor.currentState.favoriteTags.contains(where: { $0.id == model.id }) ) - object.navigationPush(tagCollectViewController, animated: true, bottomBarHidden: true) + object.parent?.navigationPush(tagCollectViewController, animated: true) } .disposed(by: self.disposeBag) @@ -221,6 +212,8 @@ class TagViewController: BaseNavigationViewController, View { .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, displayStats in + object.favoriteTagHeaderView.title = (UserDefaults.standard.nickname ?? "") + Text.favoriteTagHeaderTitle + guard let favoriteTags = displayStats.favoriteTags else { return } object.favoriteTagsView.setModels(favoriteTags) @@ -237,7 +230,7 @@ class TagViewController: BaseNavigationViewController, View { let isUpdatedWithInfo = reactor.pulse(\.$isUpdatedWithInfo).filterNil() isUpdatedWithInfo .filter { $0.isUpdated } - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, isUpdatedWithInfo in let message = isUpdatedWithInfo.model.isFavorite ? Text.addToastMessage : Text.deleteToastMessage @@ -254,7 +247,7 @@ class TagViewController: BaseNavigationViewController, View { isUpdatedWithInfo .filter { $0.isUpdated == false } - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, isUpdatedWithInfo in let actions = [ @@ -303,7 +296,13 @@ extension TagViewController: UIScrollViewDelegate { let offset = scrollView.contentOffset.y // 당겨서 새로고침 - if self.isRefreshEnabled, offset < self.initialOffset { + if self.isRefreshEnabled, offset < self.initialOffset, + let refreshControl = self.scrollView.refreshControl as? SOMRefreshControl { + + refreshControl.updateProgress( + offset: scrollView.contentOffset.y, + topInset: scrollView.adjustedContentInset.top + ) let pulledOffset = self.initialOffset - offset /// refreshControl heigt + top padding diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/TagViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Tags/TagViewReactor.swift index 52302314..794fada5 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/TagViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/TagViewReactor.swift @@ -39,21 +39,22 @@ class TagViewReactor: Reactor { ) private let dependencies: AppDIContainerable - private let tagUseCase: TagUseCase - private let userUseCase: UserUseCase + private let fetchUserInfoUseCase: FetchUserInfoUseCase + private let fetchTagUseCase: FetchTagUseCase + private let updateTagFavoriteUseCase: UpdateTagFavoriteUseCase init(dependencies: AppDIContainerable) { self.dependencies = dependencies - self.tagUseCase = dependencies.rootContainer.resolve(TagUseCase.self) - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) + self.fetchUserInfoUseCase = dependencies.rootContainer.resolve(FetchUserInfoUseCase.self) + self.fetchTagUseCase = dependencies.rootContainer.resolve(FetchTagUseCase.self) + self.updateTagFavoriteUseCase = dependencies.rootContainer.resolve(UpdateTagFavoriteUseCase.self) } func mutate(action: Action) -> Observable { switch action { case .landing: - return self.userUseCase.profile(userId: nil) - .map(\.nickname) + return self.fetchUserInfoUseCase.myNickname() .withUnretained(self) .flatMapLatest { object, nickname -> Observable in @@ -84,8 +85,10 @@ class TagViewReactor: Reactor { return .concat([ .just(.updateIsFavorite(nil)), - self.tagUseCase.updateFavorite(tagId: model.id, isFavorite: !model.isFavorite) - .flatMapLatest { isUpdated -> Observable in .just(.updateIsFavorite((model, isUpdated))) } + self.updateTagFavoriteUseCase.updateFavorite(tagId: model.id, isFavorite: !model.isFavorite) + .flatMapLatest { isUpdated -> Observable in + return .just(.updateIsFavorite((model, isUpdated))) + } .catchAndReturn(.updateIsFavorite((model, false))) ]) } @@ -111,23 +114,14 @@ private extension TagViewReactor { func favoriteTags() -> Observable { - return self.tagUseCase.favorites() - // 관심 태그는 최대 9개까지 표시 - .map { Array($0.prefix(9)).map { FavoriteTagViewModel(id: $0.id, text: $0.title) } } + return self.fetchTagUseCase.favorites() + .map { favorites in favorites.map { FavoriteTagViewModel(id: $0.id, text: $0.title) } } .map(Mutation.favoriteTags) } func popularTags() -> Observable { - return self.tagUseCase.ranked() - // 인기 태그는 최소 1개 이상일 때 표시 - .map { $0.filter { $0.usageCnt > 0 } } - // // 중복 제거 - // .map { Array(Set($0)) } - // // 태그 갯수로 정렬 - // .map { $0.sorted(by: { $0.usageCnt > $1.usageCnt }) } - // 인기 태그는 최대 10개까지 표시 - .map { Array($0.prefix(10)) } + return self.fetchTagUseCase.ranked() .map(Mutation.popularTags) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/SelectImage/WriteCardSelectImageView.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/SelectImage/WriteCardSelectImageView.swift index a6a485ae..070838ee 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/SelectImage/WriteCardSelectImageView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/SelectImage/WriteCardSelectImageView.swift @@ -23,6 +23,7 @@ class WriteCardSelectImageView: UIView { static let foodTitle: String = "푸드" static let abstractTitle: String = "추상" static let memoTitle: String = "메모" + static let eventTitle: String = "이벤트" } enum Section: Int, CaseIterable { @@ -32,6 +33,7 @@ class WriteCardSelectImageView: UIView { case food case abstract case memo + case event } enum Item: Hashable { @@ -41,6 +43,7 @@ class WriteCardSelectImageView: UIView { case food(ImageUrlInfo) case abstract(ImageUrlInfo) case memo(ImageUrlInfo) + case event(ImageUrlInfo) case user } @@ -54,14 +57,13 @@ class WriteCardSelectImageView: UIView { } private lazy var headerView = SOMSwipableTabBar().then { - $0.items = [Text.colorTitle, Text.natureTitle, Text.sensitivitytitle, Text.foodTitle, Text.abstractTitle, Text.memoTitle] $0.delegate = self } private lazy var collectionView = UICollectionView( frame: .zero, collectionViewLayout: UICollectionViewFlowLayout().then { - $0.scrollDirection = .horizontal + $0.scrollDirection = .vertical $0.minimumLineSpacing = 0 $0.minimumInteritemSpacing = 0 } @@ -137,6 +139,13 @@ class WriteCardSelectImageView: UIView { cell.setModel(imageInfo) cell.isSelected = imageInfo == self.selectedImageInfo.value?.info + return cell + case let .event(imageInfo): + + let cell: WriteCardDefaultImageCell = self.cellForDefault(collectionView, with: indexPath) + cell.setModel(imageInfo) + cell.isSelected = imageInfo == self.selectedImageInfo.value?.info + return cell case .user: @@ -207,6 +216,20 @@ class WriteCardSelectImageView: UIView { var snapshot = Snapshot() snapshot.appendSections(Section.allCases) + var items = [ + Text.colorTitle, + Text.natureTitle, + Text.sensitivitytitle, + Text.foodTitle, + Text.abstractTitle, + Text.memoTitle, + Text.eventTitle + ] + if models.event == nil { + snapshot.deleteSections([.event]) + items.removeAll(where: { $0 == Text.eventTitle }) + } + self.headerView.items = items var new = models.color.map { Item.color($0) } new.append(Item.user) @@ -237,7 +260,8 @@ class WriteCardSelectImageView: UIView { let .sensitivity(imageInfo), let .food(imageInfo), let .abstract(imageInfo), - let .memo(imageInfo): + let .memo(imageInfo), + let .event(imageInfo): return imageInfo == self.selectedImageInfo.value?.info case .user: return false @@ -306,6 +330,10 @@ extension WriteCardSelectImageView: SOMSwipableTabBarDelegate { case 5: let items = self.models.memo.map { Item.memo($0) } return (.memo, items) + case 6: + guard let events = self.models.event else { return nil } + let items = events.map { Item.event($0) } + return (.event, items) default: return nil } @@ -327,7 +355,8 @@ extension WriteCardSelectImageView: SOMSwipableTabBarDelegate { let .sensitivity(imageInfo), let .food(imageInfo), let .abstract(imageInfo), - let .memo(imageInfo): + let .memo(imageInfo), + let .event(imageInfo): return imageInfo == self.selectedImageInfo.value?.info case .user: return false @@ -357,7 +386,8 @@ extension WriteCardSelectImageView: UICollectionViewDelegate { let .sensitivity(imageInfo), let .food(imageInfo), let .abstract(imageInfo), - let .memo(imageInfo): + let .memo(imageInfo), + let .event(imageInfo): return imageInfo == self.selectedImageInfo.value?.info case .user: return false @@ -379,6 +409,8 @@ extension WriteCardSelectImageView: UICollectionViewDelegate { self.selectedImageInfo.accept((.default, imageInfo)) case let .memo(imageInfo): self.selectedImageInfo.accept((.default, imageInfo)) + case let .event(imageInfo): + self.selectedImageInfo.accept((.default, imageInfo)) case .user: self.selectedUseUserImageCell.accept(()) } diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift index 80c30ac5..375373fe 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift @@ -24,7 +24,7 @@ class WriteCardTextView: UIView { private lazy var backgroundImageView = UIImageView().then { $0.backgroundColor = .clear $0.contentMode = .scaleAspectFill - $0.layer.borderColor = UIColor.som.v2.white.cgColor + $0.layer.borderColor = UIColor.som.v2.gray100.cgColor $0.layer.borderWidth = 1 $0.layer.cornerRadius = 16 $0.clipsToBounds = true @@ -50,11 +50,14 @@ class WriteCardTextView: UIView { $0.textContainerInset = .init(top: 20, left: 24, bottom: 20, right: 24) $0.textContainer.lineFragmentPadding = 0 + $0.textContainer.lineBreakMode = .byCharWrapping $0.scrollIndicatorInsets = .init(top: 20, left: 0, bottom: 20, right: 0) $0.indicatorStyle = .white $0.isScrollEnabled = false + $0.showsHorizontalScrollIndicator = false + $0.returnKeyType = .default $0.autocapitalizationType = .none @@ -108,8 +111,13 @@ class WriteCardTextView: UIView { var typography: Typography = .som.v2.body1 { didSet { - self.placeholder = self.placeholderLabel.text - self.text = self.textView.text + self.textView.typography = self.typography + self.placeholderLabel.typography = self.typography + + let limit = self.typography.lineHeight * 8 + 20 * 2 + self.backgroundDimView.snp.updateConstraints { + $0.height.lessThanOrEqualTo(limit) + } } } @@ -119,11 +127,6 @@ class WriteCardTextView: UIView { weak var delegate: WritrCardTextViewDelegate? - // MARK: Constraint - - private var textViewBackgroundHeightConstraint: Constraint? - - // MARK: Override func override func touchesBegan(_ touches: Set, with event: UIEvent?) { } @@ -166,7 +169,8 @@ class WriteCardTextView: UIView { $0.centerY.equalToSuperview() $0.leading.equalToSuperview().offset(32) $0.trailing.equalToSuperview().offset(-32) - self.textViewBackgroundHeightConstraint = $0.height.equalTo(self.typography.lineHeight + 20 * 2).constraint + let limit = self.typography.lineHeight * 8 + 20 * 2 + $0.height.lessThanOrEqualTo(limit) } self.backgroundDimView.addSubview(self.textView) @@ -180,30 +184,23 @@ class WriteCardTextView: UIView { } } - private func updateTextContainerInsetAndHeight(_ textView: UITextView) { + private func updateTextContainerHeightLimit(_ textView: UITextView) { - var attributes = self.typography.attributes - attributes[.font] = self.typography.font let attributedText = NSAttributedString( string: textView.text, - attributes: attributes + attributes: self.typography.attributes ) /// width 계산 시 textContainerInset 고려 let textSize: CGSize = .init(width: textView.bounds.width - 24 * 2, height: .greatestFiniteMagnitude) - var boundingHeight = attributedText.boundingRect( + let boundingHeight = attributedText.boundingRect( with: textSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).height - /// 자연스러운 줄바꿈을 위해 offset 추가 - boundingHeight += 1.0 let lines: CGFloat = boundingHeight / self.typography.lineHeight let isScrollEnabled: Bool = lines > 8 - let newHeight: CGFloat = isScrollEnabled ? self.typography.lineHeight * 8 : boundingHeight - let updatedHeight: CGFloat = max(newHeight, self.typography.lineHeight) - self.textViewBackgroundHeightConstraint?.update(offset: updatedHeight + 20 * 2) textView.isScrollEnabled = isScrollEnabled } } @@ -225,7 +222,7 @@ extension WriteCardTextView: UITextViewDelegate { } func textViewDidChange(_ textView: UITextView) { - self.updateTextContainerInsetAndHeight(textView) + self.updateTextContainerHeightLimit(textView) } func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardGuideView.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardGuideView.swift new file mode 100644 index 00000000..6ba4fcf1 --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardGuideView.swift @@ -0,0 +1,54 @@ +// +// WriteCardGuideView.swift +// SOOUM +// +// Created by 오현식 on 12/5/25. +// + +import UIKit + +import SnapKit +import Then + +class WriteCardGuideView: UIView { + + + // MARK: Views + + private let imageView = UIImageView().then { + $0.image = .init(.image(.v2(.guide_write_card))) + // $0.contentMode = .scaleAspectFit + } + + let closeButton = UIButton() + + + // MARK: Initialize + + override init(frame: CGRect) { + super.init(frame: frame) + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Private func + + private func setupConstraints() { + + self.addSubview(self.imageView) + self.imageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + self.addSubview(self.closeButton) + self.closeButton.snp.makeConstraints { + $0.top.equalToSuperview().offset(60) + $0.leading.equalToSuperview().offset(4) + $0.size.equalTo(48) + } + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardView.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardView.swift index f0c78d60..ffa3e46d 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/WriteCardView.swift @@ -38,6 +38,7 @@ class WriteCardView: UIView { var textFieldDidBeginEditing = PublishRelay() var textDidChanged = BehaviorRelay(value: nil) + // MARK: Initialize override init(frame: CGRect) { diff --git a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift index dee5edc5..1a3a100b 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift @@ -68,6 +68,8 @@ class WriteCardViewController: BaseNavigationViewController, View { // MARK: Views + private let writeCardGuideView = WriteCardGuideView() + private let writeButton = SOMButton().then { $0.title = Text.navigationWriteButtonTitle $0.typography = .som.v2.subtitle1 @@ -181,12 +183,23 @@ class WriteCardViewController: BaseNavigationViewController, View { self.relatedTagsViewBottomConstraint = $0.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).constraint $0.horizontalEdges.equalToSuperview() } + + guard let windowScene: UIWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window: UIWindow = windowScene.windows.first(where: { $0.isKeyWindow }) + else { return } + + window.addSubview(self.writeCardGuideView) + self.writeCardGuideView.snp.makeConstraints { + $0.edges.equalToSuperview() + } } override func viewDidLoad() { super.viewDidLoad() PHPhotoLibrary.requestAuthorization(for: .readWrite) { _ in } + + self.writeCardGuideView.isHidden = UserDefaults.showGuideView == false } override func updatedKeyboard(withoutBottomSafeInset height: CGFloat) { @@ -201,6 +214,12 @@ class WriteCardViewController: BaseNavigationViewController, View { func bind(reactor: WriteCardViewReactor) { + self.writeCardGuideView.closeButton.rx.tap + .subscribe(with: self) { object, _ in + object.writeCardGuideView.isHidden = true + } + .disposed(by: self.disposeBag) + var options: [SelectOptionItem.OptionType] { if reactor.entranceType == .feed { return [.distanceShare, .story] @@ -211,20 +230,20 @@ class WriteCardViewController: BaseNavigationViewController, View { self.selectOptionsView.items = options self.writeCardView.textViewDidBeginEditing - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in object.isScrollingByFirstResponder = true object.scrollContainer.setContentOffset(.zero, animated: true) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - object.isScrollingByFirstResponder = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak object] in + object?.isScrollingByFirstResponder = false } } .disposed(by: self.disposeBag) self.relatedTagsView.updatedContentHeight .distinctUntilChanged() - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, updatedContentHeight in object.isScrollingByFirstResponder = true @@ -241,16 +260,16 @@ class WriteCardViewController: BaseNavigationViewController, View { object.scrollContainer.setContentOffset(scrollTo, animated: true) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - object.isScrollingByFirstResponder = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak object] in + object?.isScrollingByFirstResponder = false } } else { - DispatchQueue.main.async { - object.scrollContainer.setContentOffset(.zero, animated: true) + DispatchQueue.main.async { [weak object] in + object?.scrollContainer.setContentOffset(.zero, animated: true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - object.isScrollingByFirstResponder = false + object?.isScrollingByFirstResponder = false } } } @@ -275,6 +294,7 @@ class WriteCardViewController: BaseNavigationViewController, View { let selectedRelatedTag = self.relatedTagsView.selectedRelatedTag.filterNil().share() selectedRelatedTag + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in object.writeCardView.writeCardTags.updateFooterText = nil } @@ -305,7 +325,7 @@ class WriteCardViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) self.selectImageView.selectedUseUserImageCell - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, _ in let status = PHPhotoLibrary.authorizationStatus(for: .readWrite) @@ -388,7 +408,6 @@ class WriteCardViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) // Action - let viewDidLoad = self.rx.viewDidLoad.share() viewDidLoad .map { _ in Reactor.Action.landing } @@ -409,7 +428,8 @@ class WriteCardViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) - self.writeCardView.textDidChanged + let enteredTag = self.writeCardView.textDidChanged.share() + enteredTag .filterNil() .distinctUntilChanged() .debounce(.milliseconds(500), scheduler: MainScheduler.instance) @@ -421,14 +441,19 @@ class WriteCardViewController: BaseNavigationViewController, View { writeCardtext, selectedImageInfo.filterNil(), selectedTypography, - selectedOptions + selectedOptions, + enteredTag.startWith(nil) ) self.writeButton.rx.throttleTap(.seconds(3)) .withLatestFrom(combined) .withUnretained(self) .map { object, combined in - let (content, imageInfo, typography, options) = combined + let (content, imageInfo, typography, options, enteredTag) = combined + var enteredTagTexts = object.writeCardView.writeCardTags.models.map { $0.originalText } + if let enteredTag = enteredTag, enteredTag.isEmpty == false { + enteredTagTexts.append(enteredTag) + } return Reactor.Action.writeCard( isDistanceShared: options.contains(.distanceShare), content: content, @@ -436,14 +461,13 @@ class WriteCardViewController: BaseNavigationViewController, View { imageType: imageInfo.type, imageName: imageInfo.info.imgName, isStory: options.contains(.story), - tags: object.writeCardView.writeCardTags.models.map { $0.originalText } + tags: enteredTagTexts ) } .bind(to: reactor.action) .disposed(by: self.disposeBag) // State - reactor.state.map(\.isProcessing) .distinctUntilChanged() .observe(on: MainScheduler.asyncInstance) @@ -461,9 +485,8 @@ class WriteCardViewController: BaseNavigationViewController, View { reactor.state.map(\.writtenCardId) .filterNil() .distinctUntilChanged() - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, writtenCardId in - NotificationCenter.default.post(name: .reloadData, object: nil, userInfo: nil) if reactor.entranceType == .comment { NotificationCenter.default.post(name: .reloadDetailData, object: nil, userInfo: nil) } @@ -490,7 +513,7 @@ class WriteCardViewController: BaseNavigationViewController, View { reactor.state.map(\.hasErrors) .filterNil() .distinctUntilChanged() - .observe(on: MainScheduler.asyncInstance) + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, hasErrors in if case 422 = hasErrors { object.showInappositeDialog() @@ -507,6 +530,7 @@ class WriteCardViewController: BaseNavigationViewController, View { reactor.state.map(\.couldPosting) .filterNil() .filter { $0.isBaned } + .observe(on: MainScheduler.instance) .subscribe(with: self) { object, postingPermission in let banEndGapToDays = postingPermission.expiredAt?.infoReadableTimeTakenFromThisForBanEndPosting(to: Date().toKorea()) @@ -543,11 +567,13 @@ class WriteCardViewController: BaseNavigationViewController, View { let relatedTags = reactor.state.map(\.relatedTags).filterNil().distinctUntilChanged().share() relatedTags .map { $0.isEmpty } + .observe(on: MainScheduler.asyncInstance) .bind(to: self.relatedTagsView.rx.isHidden) .disposed(by: self.disposeBag) relatedTags .map { $0.map { RelatedTagViewModel(originalText: $0.name, count: "\($0.usageCnt)") } } + .observe(on: MainScheduler.asyncInstance) .bind(to: self.relatedTagsView.rx.models()) .disposed(by: self.disposeBag) } @@ -674,10 +700,9 @@ extension WriteCardViewController { style: .primary, action: { UIApplication.topViewController?.dismiss(animated: true) { - NotificationCenter.default.post(name: .reloadData, object: nil, userInfo: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in - self?.navigationPopToRoot(animated: true, bottomBarHidden: false) + self?.navigationPopToRoot() } } } @@ -736,8 +761,8 @@ extension WriteCardViewController { picker?.dismiss(animated: true, completion: nil) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self.present(picker, animated: true, completion: nil) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in + self?.present(picker, animated: true, completion: nil) } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift index bb3778d9..3436c4b1 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift @@ -53,10 +53,11 @@ class WriteCardViewReactor: Reactor { var initialState: State private let dependencies: AppDIContainerable - private let cardUseCase: CardUseCase - private let tagUseCase: TagUseCase - private let userUseCase: UserUseCase - private let settingsUseCase: SettingsUseCase + private let cardImageUseCase: CardImageUseCase + private let writeCardUseCase: WriteCardUseCase + private let fetchTagUseCase: FetchTagUseCase + private let validateUserUseCase: ValidateUserUseCase + private let locationUseCase: LocationUseCase let entranceType: EntranceCardType @@ -68,15 +69,17 @@ class WriteCardViewReactor: Reactor { parentCardId: String? = nil ) { self.dependencies = dependencies - self.cardUseCase = dependencies.rootContainer.resolve(CardUseCase.self) - self.tagUseCase = dependencies.rootContainer.resolve(TagUseCase.self) - self.userUseCase = dependencies.rootContainer.resolve(UserUseCase.self) - self.settingsUseCase = dependencies.rootContainer.resolve(SettingsUseCase.self) + self.cardImageUseCase = dependencies.rootContainer.resolve(CardImageUseCase.self) + self.writeCardUseCase = dependencies.rootContainer.resolve(WriteCardUseCase.self) + self.fetchTagUseCase = dependencies.rootContainer.resolve(FetchTagUseCase.self) + self.validateUserUseCase = dependencies.rootContainer.resolve(ValidateUserUseCase.self) + self.locationUseCase = dependencies.rootContainer.resolve(LocationUseCase.self) + self.entranceType = entranceType self.parentCardId = parentCardId self.initialState = State( - hasPermission: self.settingsUseCase.hasPermission(), + hasPermission: self.locationUseCase.hasPermission(), shouldUseCoordinates: false, defaultImages: nil, userImage: nil, @@ -93,7 +96,7 @@ class WriteCardViewReactor: Reactor { switch action { case .landing: - return self.cardUseCase.defaultImages().map(Mutation.defaultImages) + return self.cardImageUseCase.defaultImages().map(Mutation.defaultImages) case let .updateUserImage(userImage, isDownloaded): @@ -126,14 +129,14 @@ class WriteCardViewReactor: Reactor { ]) case let .relatedTags(keyword): - return self.tagUseCase.related(keyword: keyword, size: 8) + return self.fetchTagUseCase.related(keyword: keyword, size: 8) .map(Mutation.relatedTags) case .updateRelatedTags: return .just(.relatedTags([])) case .postingPermission: - return self.userUseCase.postingPermission() + return self.validateUserUseCase.postingPermission() .map(Mutation.updatePostingPermission) } } @@ -165,13 +168,13 @@ private extension WriteCardViewReactor { func uploadImage(_ image: UIImage) -> Observable { - return self.cardUseCase.presignedURL() + return self.cardImageUseCase.presignedURL() .withUnretained(self) .flatMapLatest { object, presignedInfo -> Observable in if let imageData = image.jpegData(compressionQuality: 0.5), let url = URL(string: presignedInfo.imgUrl) { - return object.cardUseCase.uploadImage(imageData, with: url) + return object.cardImageUseCase.uploadToS3(imageData, with: url) .flatMapLatest { isSuccess -> Observable in let imageName = isSuccess ? presignedInfo.imgName : nil @@ -193,14 +196,14 @@ private extension WriteCardViewReactor { tags: [String] ) -> Observable { - let coordinate = self.settingsUseCase.coordinate() + let coordinate = self.locationUseCase.coordinate() let trimedContent = content.trimmingCharacters(in: .whitespacesAndNewlines) if case .default = imageType, let imageName = imageName { if self.entranceType == .feed { - return self.cardUseCase.writeCard( + return self.writeCardUseCase.writeFeed( isDistanceShared: isDistanceShared, latitude: coordinate.latitude, longitude: coordinate.longitude, @@ -214,8 +217,8 @@ private extension WriteCardViewReactor { .map(Mutation.writeCard) } else { - return self.cardUseCase.writeComment( - id: self.parentCardId ?? "", + return self.writeCardUseCase.writeComment( + parentCardId: self.parentCardId ?? "", isDistanceShared: isDistanceShared, latitude: coordinate.latitude, longitude: coordinate.longitude, @@ -238,7 +241,7 @@ private extension WriteCardViewReactor { if self.entranceType == .feed { - return object.cardUseCase.writeCard( + return object.writeCardUseCase.writeFeed( isDistanceShared: isDistanceShared, latitude: coordinate.latitude, longitude: coordinate.longitude, @@ -252,8 +255,8 @@ private extension WriteCardViewReactor { .map(Mutation.writeCard) } else { - return object.cardUseCase.writeComment( - id: object.parentCardId ?? "", + return object.writeCardUseCase.writeComment( + parentCardId: object.parentCardId ?? "", isDistanceShared: isDistanceShared, latitude: coordinate.latitude, longitude: coordinate.longitude, @@ -279,7 +282,7 @@ private extension WriteCardViewReactor { return .concat([ .just(.writeCard(nil)), .just(.updateIsProcessing(false)), - self.userUseCase.postingPermission() + self.validateUserUseCase.postingPermission() .map(Mutation.updatePostingPermission) ]) } @@ -297,6 +300,6 @@ private extension WriteCardViewReactor { extension WriteCardViewReactor { func reactorForDetail(with targetCardId: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, self.entranceType, type: .navi, with: targetCardId) + DetailViewReactor(dependencies: self.dependencies, with: targetCardId) } } diff --git a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift index bb52aa2b..ba7e64dd 100644 --- a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift +++ b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift @@ -25,7 +25,10 @@ enum CardRequest: BaseRequest { // MARK: Detail + /// 상세보기 case detailCard(id: String, latitude: String?, longitude: String?) + /// 상세보기 - 삭제 여부 + case isCardDeleted(id: String) /// 상세보기 - 답카드 case commentCard(id: String, lastId: String?, latitude: String?, longitude: String?) /// 상세보기 - 카드 삭제 @@ -99,6 +102,9 @@ enum CardRequest: BaseRequest { case let .deleteCard(id): return "/api/cards/\(id)" + case let .isCardDeleted(id): + + return "/api/cards/\(id)/delete-check" case let .updateLike(id, _): return "/api/cards/\(id)/like" diff --git a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift index 8cb8b064..7cf83112 100644 --- a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift +++ b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift @@ -87,9 +87,13 @@ enum UserRequest: BaseRequest { } else { return "/api/members/\(userId)/cards/feed" } - case .myCommentCards: + case let .myCommentCards(lastId): - return "/api/members/me/cards/comment" + if let lastId = lastId { + return "/api/members/me/cards/comment/\(lastId)" + } else { + return "/api/members/me/cards/comment" + } case let .followers(userId, lastId): if let lastId = lastId { diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/Contents.json new file mode 100644 index 00000000..5949e4d6 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/Contents.json @@ -0,0 +1,27 @@ +{ + "images" : [ + { + "filename" : "v2_guide_write_card.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "v2_guide_write_card_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "v2_guide_write_card_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card.png b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card.png new file mode 100644 index 00000000..964ecd04 Binary files /dev/null and b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card.png differ diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card_2x.png b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card_2x.png new file mode 100644 index 00000000..21847b8b Binary files /dev/null and b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card_2x.png differ diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card_3x.png b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card_3x.png new file mode 100644 index 00000000..f63b1dc1 Binary files /dev/null and b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_guide_write_card.imageset/v2_guide_write_card_3x.png differ diff --git a/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift b/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift index ab2b4b7e..87c22929 100644 --- a/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift +++ b/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift @@ -19,6 +19,11 @@ extension UITextView { objc_setAssociatedObject(self, &Self.kUITextViewTypography, typography, .OBJC_ASSOCIATION_RETAIN) var attributes: [NSAttributedString.Key: Any] = typography.attributes + var baselineOffset = attributes[.baselineOffset] as! CGFloat + baselineOffset -= abs(typography.font.descender) + attributes.removeValue(forKey: .baselineOffset) + + attributes[.baselineOffset] = baselineOffset attributes[.font] = typography.font attributes[.foregroundColor] = self.textColor closure?(&attributes) @@ -27,9 +32,6 @@ extension UITextView { if let text = self.text, text.isEmpty == false { let selectedRange = self.selectedRange - // TODO: 임시, 줄바꿈 시 겹치는 문제 해결 - attributes[.baselineOffset] = attributes[.baselineOffset] as! CGFloat * -0.1 - let attributedText = NSMutableAttributedString(string: text, attributes: attributes) self.attributedText = attributedText self.selectedRange = selectedRange