diff --git a/SOOUM/SOOUM.xcodeproj/project.pbxproj b/SOOUM/SOOUM.xcodeproj/project.pbxproj index f2fe2c45..1cbb1dd3 100644 --- a/SOOUM/SOOUM.xcodeproj/project.pbxproj +++ b/SOOUM/SOOUM.xcodeproj/project.pbxproj @@ -638,6 +638,10 @@ 38D2FBCF2E81B52F006DD739 /* SOMSwipableTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBCD2E81B529006DD739 /* SOMSwipableTabBar.swift */; }; 38D2FBD12E81B9B7006DD739 /* SOMSwipableTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBD02E81B9B0006DD739 /* SOMSwipableTabBarDelegate.swift */; }; 38D2FBD22E81B9B7006DD739 /* SOMSwipableTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBD02E81B9B0006DD739 /* SOMSwipableTabBarDelegate.swift */; }; + 38D478072EBBAA0B0041FF6C /* WriteCardResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D478062EBBAA080041FF6C /* WriteCardResponse.swift */; }; + 38D478082EBBAA0B0041FF6C /* WriteCardResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D478062EBBAA080041FF6C /* WriteCardResponse.swift */; }; + 38D4780A2EBBABF60041FF6C /* EntranceCardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D478092EBBABE40041FF6C /* EntranceCardType.swift */; }; + 38D4780B2EBBABF60041FF6C /* EntranceCardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D478092EBBABE40041FF6C /* EntranceCardType.swift */; }; 38D488CA2D0C557300F2D38D /* SOMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D488C92D0C557300F2D38D /* SOMButton.swift */; }; 38D488CB2D0C557300F2D38D /* SOMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D488C92D0C557300F2D38D /* SOMButton.swift */; }; 38D522682E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D522672E742F550044911B /* SOMLoadingIndicatorView.swift */; }; @@ -1095,6 +1099,8 @@ 38D2FBCA2E81B0DE006DD739 /* SOMSwipableTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMSwipableTabBarItem.swift; sourceTree = ""; }; 38D2FBCD2E81B529006DD739 /* SOMSwipableTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMSwipableTabBar.swift; sourceTree = ""; }; 38D2FBD02E81B9B0006DD739 /* SOMSwipableTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMSwipableTabBarDelegate.swift; sourceTree = ""; }; + 38D478062EBBAA080041FF6C /* WriteCardResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardResponse.swift; sourceTree = ""; }; + 38D478092EBBABE40041FF6C /* EntranceCardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntranceCardType.swift; sourceTree = ""; }; 38D488C92D0C557300F2D38D /* SOMButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMButton.swift; sourceTree = ""; }; 38D522672E742F550044911B /* SOMLoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMLoadingIndicatorView.swift; sourceTree = ""; }; 38D5637A2D16D72D006265AA /* SOMStickyTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMStickyTabBar.swift; sourceTree = ""; }; @@ -2170,6 +2176,7 @@ 38899E632E7938CD0030F7CA /* Responses */ = { isa = PBXGroup; children = ( + 38D478062EBBAA080041FF6C /* WriteCardResponse.swift */, 38E928BE2EB72D3600B3F00B /* DetailCardInfoResponse.swift */, 38EBA9102EB3999C008B28F4 /* PostingPermissionResponse.swift */, 38C9AF0D2E96601E00B401C0 /* DefaultImagesResponse.swift */, @@ -2207,6 +2214,7 @@ 38899E692E793AEA0030F7CA /* Models */ = { isa = PBXGroup; children = ( + 38D478092EBBABE40041FF6C /* EntranceCardType.swift */, 38E928B82EB715C300B3F00B /* ReortType.swift */, 38E928B52EB711DE00B3F00B /* DetailCardInfo.swift */, 38EBA90D2EB39917008B28F4 /* PostingPermission.swift */, @@ -3253,6 +3261,7 @@ 388D8AE02E73E6190044BA79 /* SwiftEntryKit.swift in Sources */, 38899E962E7953310030F7CA /* NotificationInfoResponse.swift in Sources */, 38FEBE5F2E86612C002916A8 /* NoticeViewCell.swift in Sources */, + 38D4780A2EBBABF60041FF6C /* EntranceCardType.swift in Sources */, 2AFD055A2D008D23007C84AD /* TagDetailViewController.swift in Sources */, 2AFF95562CF3222400CBFB12 /* TagsViewController.swift in Sources */, 38C9AF182E96693600B401C0 /* TagRemoteDataSource.swift in Sources */, @@ -3311,6 +3320,7 @@ 3878D0862CFFED7800F9522F /* TermsOfServiceTextCellView.swift in Sources */, 38899E832E794C360030F7CA /* LoginResponse.swift in Sources */, 2ACBD41B2CCA03790057C013 /* ImageURLWithName.swift in Sources */, + 38D478072EBBAA0B0041FF6C /* WriteCardResponse.swift in Sources */, 38E928C02EB72D3D00B3F00B /* DetailCardInfoResponse.swift in Sources */, 38B6AAE02CA4777200CE6DB6 /* UIViewController+Rx.swift in Sources */, 2ACBD41E2CCAB3490057C013 /* PresignedStorageResponse.swift in Sources */, @@ -3634,6 +3644,7 @@ 38FEBE5E2E86612C002916A8 /* NoticeViewCell.swift in Sources */, 38F3D9302D06C2370049F575 /* SOMAnimationTransitioning.swift in Sources */, 38C9AF172E96693600B401C0 /* TagRemoteDataSource.swift in Sources */, + 38D4780B2EBBABF60041FF6C /* EntranceCardType.swift in Sources */, 385602B62D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */, 38E9CE192D37FED000E85A2D /* AddingTokenInterceptor.swift in Sources */, 380F42242E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */, @@ -3692,6 +3703,7 @@ 2AFF955D2CF328DE00CBFB12 /* FavoriteTagTableViewCell.swift in Sources */, 38899E842E794C360030F7CA /* LoginResponse.swift in Sources */, 385053522C92DBE200C80B02 /* SOMTabBarItem.swift in Sources */, + 38D478082EBBAA0B0041FF6C /* WriteCardResponse.swift in Sources */, 38E928BF2EB72D3D00B3F00B /* DetailCardInfoResponse.swift in Sources */, 38A5D1542C8CB11E00B68363 /* UIImage+SOOUM.swift in Sources */, 38601E182D31399400A465A9 /* CardRequest.swift in Sources */, @@ -3788,7 +3800,7 @@ CODE_SIGN_ENTITLEMENTS = "SOOUM/Resources/SOOUM-Dev.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1017000; + CURRENT_PROJECT_VERSION = 1017040; DEVELOPMENT_TEAM = 99FRG743RX; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SOOUM/Resources/Develop/Info-dev.plist"; @@ -3811,7 +3823,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.17.0; + MARKETING_VERSION = 1.17.4; OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3840,7 +3852,7 @@ CODE_SIGN_ENTITLEMENTS = "SOOUM/Resources/SOOUM-Dev.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1017000; + CURRENT_PROJECT_VERSION = 1017040; DEVELOPMENT_TEAM = 99FRG743RX; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SOOUM/Resources/Develop/Info-dev.plist"; @@ -3863,7 +3875,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.17.0; + MARKETING_VERSION = 1.17.4; 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 650b8af7..95137f10 100644 --- a/SOOUM/SOOUM/Base/BaseViewController.swift +++ b/SOOUM/SOOUM/Base/BaseViewController.swift @@ -38,13 +38,9 @@ class BaseViewController: UIViewController { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - /// Show deinit class name + /// show deinit class name and remove all observer deinit { - NotificationCenter.default.removeObserver( - self, - name: .hidesBottomBarWhenPushedDidChange, - object: nil - ) + NotificationCenter.default.removeObserver(self) Log.debug("Deinit: ", type(of: self).description().components(separatedBy: ".").last ?? "") } diff --git a/SOOUM/SOOUM/Data/Models/Responses/WriteCardResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/WriteCardResponse.swift new file mode 100644 index 00000000..6d199afa --- /dev/null +++ b/SOOUM/SOOUM/Data/Models/Responses/WriteCardResponse.swift @@ -0,0 +1,32 @@ +// +// WriteCardResponse.swift +// SOOUM +// +// Created by 오현식 on 11/6/25. +// + +import Alamofire + +struct WriteCardResponse { + + let cardId: String +} + +extension WriteCardResponse: EmptyResponse { + + static func emptyValue() -> WriteCardResponse { + WriteCardResponse(cardId: "") + } +} + +extension WriteCardResponse: Decodable { + + enum CodingKeys: CodingKey { + case cardId + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.cardId = String(try container.decode(Int64.self, forKey: .cardId)) + } +} diff --git a/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift index be81750a..3acb017f 100644 --- a/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift @@ -76,6 +76,16 @@ class CardRepositoryImpl: CardRepository { return self.remoteDataSource.defaultImages() } + func presignedURL() -> Observable { + + return self.remoteDataSource.presignedURL() + } + + func uploadImage(_ data: Data, with url: URL) -> Observable> { + + return self.remoteDataSource.uploadImage(data, with: url) + } + func writeCard( isDistanceShared: Bool, latitude: String?, @@ -86,7 +96,7 @@ class CardRepositoryImpl: CardRepository { imgName: String, isStory: Bool, tags: [String] - ) -> Observable { + ) -> Observable { return self.remoteDataSource.writeCard( isDistanceShared: isDistanceShared, @@ -111,7 +121,7 @@ class CardRepositoryImpl: CardRepository { imgType: String, imgName: String, tags: [String] - ) -> Observable { + ) -> Observable { return self.remoteDataSource.writeComment( id: id, diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift index b6eeb03c..a8f96462 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift @@ -86,6 +86,17 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { return self.provider.networkManager.fetch(DefaultImagesResponse.self, request: request) } + func presignedURL() -> Observable { + + let request: CardRequest = .presignedURL + return self.provider.networkManager.fetch(ImageUrlInfoResponse.self, request: request) + } + + func uploadImage(_ data: Data, with url: URL) -> Observable> { + + return self.provider.networkManager.upload(data, to: url) + } + func writeCard( isDistanceShared: Bool, latitude: String?, @@ -96,7 +107,7 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { imgName: String, isStory: Bool, tags: [String] - ) -> Observable { + ) -> Observable { let request: CardRequest = .writeCard( isDistanceShared: isDistanceShared, @@ -109,7 +120,7 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { isStory: isStory, tags: tags ) - return self.provider.networkManager.perform(request) + return self.provider.networkManager.perform(WriteCardResponse.self, request: request) } func writeComment( @@ -122,7 +133,7 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { imgType: String, imgName: String, tags: [String] - ) -> Observable { + ) -> Observable { let request: CardRequest = .writeComment( id: id, @@ -135,6 +146,6 @@ class CardRemoteDataSourceImpl: CardRemoteDataSource { imgName: imgName, tags: tags ) - return self.provider.networkManager.perform(request) + return self.provider.networkManager.perform(WriteCardResponse.self, request: request) } } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift index e2969043..5a76a74b 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift @@ -32,6 +32,8 @@ protocol CardRemoteDataSource { // MARK: Write func defaultImages() -> Observable + func presignedURL() -> Observable + func uploadImage(_ data: Data, with url: URL) -> Observable> func writeCard( isDistanceShared: Bool, latitude: String?, @@ -42,7 +44,7 @@ protocol CardRemoteDataSource { imgName: String, isStory: Bool, tags: [String] - ) -> Observable + ) -> Observable func writeComment( id: String, isDistanceShared: Bool, @@ -53,5 +55,5 @@ protocol CardRemoteDataSource { imgType: String, imgName: String, tags: [String] - ) -> Observable + ) -> Observable } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift index a72a096e..aca942e8 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift @@ -32,7 +32,7 @@ class NotificationRemoteDataSoruceImpl: NotificationRemoteDataSource { func requestRead(notificationId: String) -> Observable { let request: NotificationRequest = .requestRead(notificationId: notificationId) - return self.provider.networkManager.perform(Int.self, request: request) + return self.provider.networkManager.perform(request) } func notices(lastId: String?, size: Int?) -> Observable { diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift index 758e900c..b2906a13 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/UserRemoteDataSourceImpl.swift @@ -43,7 +43,7 @@ class UserRemoteDataSourceImpl: UserRemoteDataSource { func presignedURL() -> Observable { let request: UserRequest = .presignedURL - return self.provider.networkManager.perform(ImageUrlInfoResponse.self, request: request) + return self.provider.networkManager.fetch(ImageUrlInfoResponse.self, request: request) } func uploadImage(_ data: Data, with url: URL) -> Observable> { diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMBottomFloatView.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMBottomFloatView.swift index 22fcaf6b..872a7c19 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMBottomFloatView.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMBottomFloatView.swift @@ -91,6 +91,8 @@ private extension SOMBottomFloatView { $0.contentHorizontalAlignment = .left + $0.isEnabled = action.isEnabled + $0.tag = action.tag $0.addTarget(self, action: #selector(self.tap(_:)), for: .touchUpInside) } @@ -116,6 +118,7 @@ extension SOMBottomFloatView { let tag: Int let image: UIImage? let foregroundColor: UIColor + let isEnabled: Bool let title: String let action: (() -> Void) @@ -123,12 +126,14 @@ extension SOMBottomFloatView { title: String, image: UIImage? = nil, foregroundColor: UIColor = .som.v2.gray500, + isEnabled: Bool = true, action: @escaping (() -> Void) ) { self.tag = UUID().hashValue self.title = title self.image = image self.foregroundColor = foregroundColor + self.isEnabled = isEnabled self.action = action } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMButton.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMButton.swift index 93a107a3..c66e189e 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMButton.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMButton.swift @@ -95,7 +95,14 @@ private extension SOMButton { updatedConfig?.background.backgroundColor = self.backgroundColor updatedConfig?.background.backgroundColorTransformer = UIConfigurationColorTransformer { _ in // 비활성화 상태일 때, backgroundColor - if button.isEnabled == false { return .som.v2.gray200 } + if button.isEnabled == false { + switch self.backgroundColor { + case .som.v2.black: return .som.v2.gray200 + case .som.v2.gray100: return .som.v2.gray200 + case .som.v2.white: return .som.v2.white + default: return .clear + } + } // 선택된 상태일 때, backgroundColor if button.isSelected { return .som.v2.pLight1 } // 하이라이트 상태일 때, backgroundColor @@ -115,7 +122,14 @@ private extension SOMButton { updatedConfig?.background.strokeColor = self.backgroundColor ?? .clear updatedConfig?.background.strokeColorTransformer = UIConfigurationColorTransformer { _ in // 비활성화 상태일 때, backgroundColor - if button.isEnabled == false { return .som.v2.gray200 } + if button.isEnabled == false { + switch self.backgroundColor { + case .som.v2.black: return .som.v2.gray200 + case .som.v2.gray100: return .som.v2.gray200 + case .som.v2.white: return .som.v2.white + default: return .clear + } + } // 선택된 상태일 때, backgroundColor if button.isSelected { return .som.v2.pMain } // 하이라이트 상태일 때, backgroundColor @@ -141,7 +155,16 @@ private extension SOMButton { func applyConfiguration(to configuration: inout UIButton.Configuration?) { var foregroundColor: UIColor { - return self.isEnabled ? (self.foregroundColor ?? .som.v2.white) : .som.v2.gray400 + if self.isEnabled == false { + switch self.foregroundColor { + case .som.v2.white: return .som.v2.gray400 + case .som.v2.gray600: return .som.v2.gray400 + case .som.v2.gray500: return .som.v2.gray300 + default: return .som.v2.gray300 + } + } + + return self.foregroundColor ?? .som.v2.white } if let image = self.image { diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift index cef88eec..e605b540 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift @@ -19,16 +19,21 @@ class SOMCard: UIView { static let pungedCardText: String = "카드가 삭제되었어요" } + enum CardType { + case feed + case comment + } + // MARK: Views - let shadowbackgroundView = UIView().then { + private let shadowbackgroundView = UIView().then { $0.backgroundColor = .som.v2.white $0.layer.cornerRadius = 16 } /// 배경 이미지 - let rootContainerImageView = UIImageView().then { + private let rootContainerImageView = UIImageView().then { $0.layer.cornerRadius = 16 $0.layer.borderWidth = 1 $0.contentMode = .scaleAspectFill @@ -36,160 +41,139 @@ class SOMCard: UIView { } // 본문 dim 배경 - let cardTextBackgroundBlurView = UIView().then { + private let cardTextBackgroundBlurView = UIView().then { $0.backgroundColor = .som.v2.dim $0.layer.cornerRadius = 12 $0.clipsToBounds = true } /// 본문 표시 라벨 (스크롤 X) - let cardTextContentLabel = UILabel().then { + private let cardTextContentLabel = UILabel().then { $0.textColor = .som.v2.white $0.typography = .som.v2.body1 $0.textAlignment = .center - $0.numberOfLines = 3 + $0.numberOfLines = 4 $0.lineBreakMode = .byTruncatingTail $0.lineBreakStrategy = .hangulWordPriority } - /// 본문 스크롤 텍스트 뷰 (스크롤 O) - let cardTextContentScrollView = UITextView().then { - $0.textColor = .som.v2.white - $0.typography = .som.v2.body1 - - $0.backgroundColor = .clear - $0.tintColor = .clear - - $0.textAlignment = .center - $0.textContainerInset = .init(top: 0, left: 16, bottom: 0, right: 16) - $0.textContainer.lineFragmentPadding = 0 - - $0.indicatorStyle = .white - $0.scrollIndicatorInsets = .init(top: 14, left: 0, bottom: 14, right: 0) - - $0.isScrollEnabled = false - $0.showsVerticalScrollIndicator = true - $0.showsHorizontalScrollIndicator = false - - $0.isEditable = false - } /// 펑 시간, 거리, 시간, 좋아요 수, 답글 수 정보를 담는 뷰 - let cardInfoContainer = UIView().then { + private let cardInfoContainer = UIView().then { $0.backgroundColor = .som.v2.white $0.layer.borderColor = UIColor.som.v2.white.cgColor $0.layer.borderWidth = 1 } /// 펑 시간, 거리, 시간을 담는 스택 뷰 - let cardInfoLeadingStackView = UIStackView().then { + private let cardInfoLeadingStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 4 $0.alignment = .center } /// 좋아요 수, 답글 수를 담는 스택 뷰 - let cardInfoTrailingStackView = UIStackView().then { + private let cardInfoTrailingStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 4 $0.alignment = .center } /// 어드민 정보 표시 스택뷰 - let adminStackView = UIStackView().then { + private let adminStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 2 $0.alignment = .center } /// 어드민 정보 아이콘 - let adminImageView = UIImageView().then { + private let adminImageView = UIImageView().then { $0.image = .init(.icon(.v2(.filled(.official)))) $0.tintColor = .som.v2.black } /// 어드민 정보 라벨 - let adminLabel = UILabel().then { + private let adminLabel = UILabel().then { $0.text = Text.adminTitle $0.textColor = .som.v2.black $0.typography = .som.v2.caption2 } /// 어드민 닷 - let firstDot = UIView().then { + private let firstDot = UIView().then { $0.backgroundColor = .som.v2.gray500 $0.layer.cornerRadius = 1 } /// 펑 남은시간 표시 스택뷰 - let cardPungTimeStackView = UIStackView().then { + private let cardPungTimeStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 2 $0.alignment = .center } /// 펑 남은시간 표시 아이콘 - let cardPungTimeImageView = UIImageView().then { + private let cardPungTimeImageView = UIImageView().then { $0.image = .init(.icon(.v2(.filled(.bomb)))) $0.tintColor = .som.v2.pMain } /// 펑 남은시간 표시 라벨 - let cardPungTimeLabel = UILabel().then { + private let cardPungTimeLabel = UILabel().then { $0.textColor = .som.v2.pDark $0.typography = .som.v2.caption2 } /// 펑 남은시간 닷 - let secondDot = UIView().then { + private let secondDot = UIView().then { $0.backgroundColor = .som.v2.gray500 $0.layer.cornerRadius = 1 } /// 거리 정보 표시 스택뷰 - let distanceInfoStackView = UIStackView().then { + private let distanceInfoStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 2 $0.alignment = .center } /// 거리 정보 아이콘 - let distanceImageView = UIImageView().then { + private let distanceImageView = UIImageView().then { $0.image = .init(.icon(.v2(.outlined(.location)))) $0.tintColor = .som.v2.gray500 } /// 거리 정보 라벨 - let distanceLabel = UILabel().then { + private let distanceLabel = UILabel().then { $0.textColor = .som.v2.gray500 $0.typography = .som.v2.caption2 } /// 거리 정보 닷 - let thirdDot = UIView().then { + private let thirdDot = UIView().then { $0.backgroundColor = .som.v2.gray500 $0.layer.cornerRadius = 1 } /// 시간 정보 표시 라벨 - let timeLabel = UILabel().then { + private let timeLabel = UILabel().then { $0.textColor = .som.v2.gray500 $0.typography = .som.v2.caption2 } /// 좋아요 정보 표시 스택뷰 - let likeInfoStackView = UIStackView().then { + private let likeInfoStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 2 $0.alignment = .center } /// 좋아요 정보 표시 아이콘 - let likeImageView = UIImageView().then { + private let likeImageView = UIImageView().then { $0.image = .init(.icon(.v2(.outlined(.heart)))) $0.tintColor = .som.v2.gray500 } /// 좋아요 정보 표시 라벨 - let likeLabel = UILabel().then { + private let likeLabel = UILabel().then { $0.textColor = .som.v2.gray500 $0.typography = .som.v2.caption2 } /// 답카드 정보 표시 스택뷰 - let commentInfoStackView = UIStackView().then { + private let commentInfoStackView = UIStackView().then { $0.axis = .horizontal $0.spacing = 2 $0.alignment = .center } /// 답카드 정보 표시 아이콘 - let commentImageView = UIImageView().then { + private let commentImageView = UIImageView().then { $0.image = .init(.icon(.v2(.outlined(.message_circle)))) $0.tintColor = .som.v2.gray500 } /// 답카드 정보 표시 라벨 - let commentLabel = UILabel().then { + private let commentLabel = UILabel().then { $0.textColor = .som.v2.gray500 $0.typography = .som.v2.caption2 } @@ -197,16 +181,14 @@ class SOMCard: UIView { // MARK: Variables - var model: BaseCardInfo? - - private var hasScrollEnabled: Bool + private(set) var model: BaseCardInfo = .defaultValue + private(set) var cardType: CardType // MARK: Constraints - // TODO: 카드 본문 배경 블러 뷰 높이 계산 Constraint, 헌재 사용 X + // TODO: 카드 본문 높이 계산 Constraint private var contentHeightConstraint: Constraint? - private var scrollContentHieghtConstraint: Constraint? /// 펑 이벤트 처리 위해 추가 var serialTimer: Disposable? @@ -215,8 +197,8 @@ class SOMCard: UIView { // MARK: Initialize - init(hasScrollEnabled: Bool = false) { - self.hasScrollEnabled = hasScrollEnabled + init(type cardType: CardType = .feed) { + self.cardType = cardType super.init(frame: .zero) self.setupConstraints() @@ -342,22 +324,14 @@ class SOMCard: UIView { $0.trailing.equalToSuperview().offset(-32) } - if self.hasScrollEnabled { - self.cardTextBackgroundBlurView.addSubview(self.cardTextContentScrollView) - self.cardTextContentScrollView.snp.makeConstraints { - $0.top.equalToSuperview().offset(20) - $0.bottom.equalToSuperview().offset(-20) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview().offset(-24) - } - } else { - self.cardTextBackgroundBlurView.addSubview(self.cardTextContentLabel) - self.cardTextContentLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(20) - $0.bottom.equalToSuperview().offset(-20) - $0.leading.equalToSuperview().offset(24) - $0.trailing.equalToSuperview().offset(-24) - } + self.cardTextBackgroundBlurView.addSubview(self.cardTextContentLabel) + self.cardTextContentLabel.snp.makeConstraints { + let verticalOffset: CGFloat = self.cardType == .feed ? 20 : 16 + $0.top.equalToSuperview().offset(verticalOffset) + $0.bottom.equalToSuperview().offset(-verticalOffset) + $0.leading.equalToSuperview().offset(24) + $0.trailing.equalToSuperview().offset(-24) + self.contentHeightConstraint = $0.height.equalTo(Typography.som.v2.body1.lineHeight).constraint } } @@ -368,6 +342,13 @@ class SOMCard: UIView { func prepareForReuse() { self.serialTimer?.dispose() self.disposeBag = DisposeBag() + + self.adminLabel.text = nil + self.cardPungTimeLabel.text = nil + self.distanceLabel.text = nil + self.timeLabel.text = nil + self.likeLabel.text = nil + self.commentLabel.text = nil } /// 홈피드 모델 초기화 @@ -386,13 +367,9 @@ class SOMCard: UIView { case .yoonwoo: typography = .som.v2.yoonwooCard case .kkookkkook: typography = .som.v2.kkookkkookCard } - if self.hasScrollEnabled { - self.cardTextContentScrollView.text = model.cardContent - self.cardTextContentScrollView.typography = typography - } else { - self.cardTextContentLabel.text = model.cardContent - self.cardTextContentLabel.typography = typography - } + self.cardTextContentLabel.text = model.cardContent + self.cardTextContentLabel.typography = typography + self.updateContentHeight(model.cardContent, with: typography) // 하단 정보 // 어드민, 펑 시간, 거리, 시간 @@ -461,39 +438,35 @@ class SOMCard: UIView { } // TODO: 카드 본문 배경 블러 뷰 높이 계산 함수, 헌재 사용 X - private func updateContentHeight(_ text: String) { + private func updateContentHeight(_ text: String, with typography: Typography) { - self.layoutIfNeeded() - // TODO: 임시, 폰트 가변임 - let typography = Typography.som.v2.body1 + UIView.performWithoutAnimation { + self.layoutIfNeeded() + } + var attributes = typography.attributes attributes.updateValue(typography.font, forKey: .font) let attributedText = NSAttributedString( string: text, attributes: attributes ) - - let availableWidth = UIScreen.main.bounds.width - 16 * 2 - 32 * 2 - 24 * 2 + /// screen width - SOMCard horizontal padding - text background dim view horizontal padding - text horizontal inset + let availableWidth = self.cardTextContentLabel.bounds.width let size: CGSize = .init(width: availableWidth, height: .greatestFiniteMagnitude) - let boundingRect = attributedText.boundingRect( + let boundingHeight = attributedText.boundingRect( with: size, options: [.usesLineFragmentOrigin], context: nil - ) - let boundingHeight = boundingRect.height + 20 * 2 /// top, bottom inset - let backgroundHeight = rootContainerImageView.bounds.height + ).height + let backgroundHeight = self.rootContainerImageView.bounds.height - let height = min(boundingHeight, (backgroundHeight - 34) * 0.8) + let maxHeight = self.cardType == .feed ? typography.lineHeight * 3 : typography.lineHeight * 4 + let height = min(boundingHeight, maxHeight) self.contentHeightConstraint?.update(offset: height) - if self.hasScrollEnabled { - self.cardTextContentScrollView.isScrollEnabled = boundingHeight > backgroundHeight * 0.5 - self.cardTextContentScrollView.isUserInteractionEnabled = true - self.cardTextContentScrollView.contentSize = .init( - width: cardTextContentScrollView.bounds.width, - height: boundingHeight - ) + UIView.performWithoutAnimation { + self.layoutIfNeeded() } } @@ -536,10 +509,6 @@ class SOMCard: UIView { .filter { $0 != self.cardPungTimeStackView } .forEach { $0.removeFromSuperview() } - if self.hasScrollEnabled { - self.cardTextContentScrollView.text = Text.pungedCardText - } else { - self.cardTextContentLabel.text = Text.pungedCardText - } + self.cardTextContentLabel.text = Text.pungedCardText } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift index 81e3f031..a5eeed55 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift @@ -10,6 +10,9 @@ import UIKit import SnapKit import Then + +// MARK: SOMTabBarControllerDelegate + protocol SOMTabBarControllerDelegate: AnyObject { func tabBarController( @@ -26,6 +29,9 @@ protocol SOMTabBarControllerDelegate: AnyObject { class SOMTabBarController: UIViewController { + + // MARK: Views + private lazy var tabBar = SOMTabBar().then { $0.delegate = self } @@ -34,6 +40,9 @@ class SOMTabBarController: UIViewController { $0.backgroundColor = .som.white } + + // MARK: Variables + var viewControllers: [UIViewController] = [] { didSet { self.tabBar.viewControllers = self.viewControllers } } @@ -43,6 +52,20 @@ class SOMTabBarController: UIViewController { weak var delegate: SOMTabBarControllerDelegate? + + // MARK: Deinitialize + + deinit { + NotificationCenter.default.removeObserver( + self, + name: .hidesBottomBarWhenPushedDidChange, + object: nil + ) + } + + + // MARK: Override func + override func viewDidLoad() { super.viewDidLoad() @@ -56,6 +79,9 @@ class SOMTabBarController: UIViewController { self.setupConstraints() } + + // MARK: Private func + private func setupConstraints() { self.view.addSubview(self.container) @@ -71,6 +97,9 @@ class SOMTabBarController: UIViewController { } } + + // MARK: Objc func + @objc private func hidesBottomBarWhenPushed(_ notification: Notification) { @@ -87,12 +116,18 @@ class SOMTabBarController: UIViewController { } } + + // MARK: Public func + func didSelectedIndex(_ index: Int) { self.tabBar.didSelectTabBarItem(index) } } + +// MARK: SOMTabBarDelegate + extension SOMTabBarController: SOMTabBarDelegate { func tabBar(_ tabBar: SOMTabBar, shouldSelectTabAt index: Int) -> Bool { diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift index fc3e0d44..8dd5cb80 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift @@ -17,7 +17,7 @@ class SOMTabBarItem: UIView { // MARK: Views private let imageView = UIImageView().then { - $0.tintColor = .som.gray400 + $0.tintColor = .som.v2.gray300 } private let titleLabel = UILabel().then { diff --git a/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift b/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift index 08ea7cb2..661ac12d 100644 --- a/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift +++ b/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift @@ -178,6 +178,7 @@ extension UIImage.SOOUMType { case delete case down case error + case eye case flag case hash case heart diff --git a/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift b/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift index d980a38a..7139af52 100644 --- a/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift +++ b/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift @@ -34,7 +34,7 @@ extension CommonNotificationInfo { case deleted = "DELETED" case transferSuccess = "TRANSFER_SUCCESS" case follow = "FOLLOW" - case notice = "NOTICE" + case tagUsage = "TAG_USAGE" case none = "NONE" } } diff --git a/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift b/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift index 79a3e72d..22fef140 100644 --- a/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift +++ b/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift @@ -37,7 +37,7 @@ extension CompositeNotificationInfo: Decodable { case .feedLike, .commentLike, .commentWrite: let notification = try NotificationInfoResponse(from: decoder) self = .default(notification) - // TODO: NOTICE, TRANSFER_SUCCESS 는 아직 정해지지 않음 + // TODO: TRANSFER_SUCCESS, TAG_USAGE 는 아직 정해지지 않음 default: throw DecodingError.dataCorrupted( DecodingError.Context( diff --git a/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift b/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift index 91bdcff2..d404ae85 100644 --- a/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift +++ b/SOOUM/SOOUM/Domain/Models/DetailCardInfo.swift @@ -33,6 +33,37 @@ struct DetailCardInfo: Hashable { let prevCardImgURL: String? } +extension DetailCardInfo { + + func updateLikeCnt(_ likeCnt: Int, with isLike: Bool) -> DetailCardInfo { + + return DetailCardInfo( + id: self.id, + likeCnt: likeCnt, + commentCnt: self.commentCnt, + cardImgName: self.cardImgName, + cardImgURL: self.cardImgURL, + cardContent: self.cardContent, + font: self.font, + distance: self.distance, + createdAt: self.createdAt, + storyExpirationTime: self.storyExpirationTime, + isAdminCard: self.isAdminCard, + memberId: self.memberId, + nickname: self.nickname, + profileImgURL: self.profileImgURL, + isLike: isLike, + isCommentWritten: self.isCommentWritten, + tags: self.tags, + isOwnCard: self.isOwnCard, + visitedCnt: self.visitedCnt, + prevCardId: self.prevCardId, + isPrevCardDeleted: self.isPrevCardDeleted, + prevCardImgURL: self.prevCardImgURL + ) + } +} + extension DetailCardInfo { /// 작성된 태그 struct Tag: Hashable { diff --git a/SOOUM/SOOUM/Domain/Models/EntranceCardType.swift b/SOOUM/SOOUM/Domain/Models/EntranceCardType.swift new file mode 100644 index 00000000..f825c12d --- /dev/null +++ b/SOOUM/SOOUM/Domain/Models/EntranceCardType.swift @@ -0,0 +1,13 @@ +// +// EntranceCardType.swift +// SOOUM +// +// Created by 오현식 on 11/6/25. +// + +import Foundation + +enum EntranceCardType { + case feed + case comment +} diff --git a/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift b/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift index 27d6a910..6cf18b6e 100644 --- a/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift +++ b/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift @@ -32,6 +32,8 @@ protocol CardRepository { // MARK: Write func defaultImages() -> Observable + func presignedURL() -> Observable + func uploadImage(_ data: Data, with url: URL) -> Observable> func writeCard( isDistanceShared: Bool, latitude: String?, @@ -42,7 +44,7 @@ protocol CardRepository { imgName: String, isStory: Bool, tags: [String] - ) -> Observable + ) -> Observable func writeComment( id: String, isDistanceShared: Bool, @@ -53,5 +55,5 @@ protocol CardRepository { imgType: String, imgName: String, tags: [String] - ) -> Observable + ) -> Observable } diff --git a/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift index 7cfa138e..7949655b 100644 --- a/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift +++ b/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift @@ -76,6 +76,18 @@ class CardUseCaseImpl: CardUseCase { 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 { _ in true } + .catchAndReturn(false) + } + func writeCard( isDistanceShared: Bool, latitude: String?, @@ -86,7 +98,7 @@ class CardUseCaseImpl: CardUseCase { imgName: String, isStory: Bool, tags: [String] - ) -> Observable { + ) -> Observable { return self.repository.writeCard( isDistanceShared: isDistanceShared, @@ -99,7 +111,7 @@ class CardUseCaseImpl: CardUseCase { isStory: isStory, tags: tags ) - .map { $0 == 200 } + .map { $0.cardId } } func writeComment( @@ -112,7 +124,7 @@ class CardUseCaseImpl: CardUseCase { imgType: String, imgName: String, tags: [String] - ) -> Observable { + ) -> Observable { return self.repository.writeComment( id: id, @@ -125,6 +137,6 @@ class CardUseCaseImpl: CardUseCase { imgName: imgName, tags: tags ) - .map { $0 == 200 } + .map { $0.cardId } } } diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift index ebe0374e..9d3441c5 100644 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift @@ -32,6 +32,8 @@ protocol CardUseCase { // MARK: Write func defaultImages() -> Observable + func presignedURL() -> Observable + func uploadImage(_ data: Data, with url: URL) -> Observable func writeCard( isDistanceShared: Bool, latitude: String?, @@ -42,7 +44,7 @@ protocol CardUseCase { imgName: String, isStory: Bool, tags: [String] - ) -> Observable + ) -> Observable func writeComment( id: String, isDistanceShared: Bool, @@ -53,5 +55,5 @@ protocol CardUseCase { imgType: String, imgName: String, tags: [String] - ) -> Observable + ) -> Observable } diff --git a/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift b/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift index 9c0855d2..ca7f39af 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift @@ -18,4 +18,7 @@ extension Notification.Name { static let scollingToTopWithAnimation = Notification.Name("scollingToTopWithAnimation") /// Should reload static let reloadData = Notification.Name("reloadData") + static let reloadCommentsData = Notification.Name("reloadCommentsData") + /// 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 23513f9f..80664267 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift @@ -10,18 +10,7 @@ import UIKit extension UIRefreshControl { func beginRefreshingWithOffset(_ offset: CGFloat) { - if let scrollView: UIScrollView = superview as? UIScrollView { - scrollView.contentInset.top = offset - } + self.bounds.origin.y = -offset self.beginRefreshing() - self.sendActions(for: .valueChanged) - } - - func endRefreshingWithOffset() { - - if let scrollView: UIScrollView = superview as? UIScrollView { - scrollView.contentInset.top = 0 - } - self.endRefreshing() } } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift index ded15cfe..58ccddca 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift @@ -60,7 +60,6 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, private let profileImageView = UIImageView().then { $0.image = .init(.image(.v2(.profile_large))) - // TODO: 임시, backgroundColor 넣어서 이미지 빈 곳 채움 $0.backgroundColor = .som.v2.gray300 $0.layer.cornerRadius = 120 * 0.5 $0.layer.borderWidth = 1 @@ -135,6 +134,12 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, } } + override func viewDidLoad() { + super.viewDidLoad() + + PHPhotoLibrary.requestAuthorization(for: .readWrite) { _ in } + } + // MARK: ReactorKit - bind diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift index 7fbfbc27..b77a3bf4 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift @@ -24,13 +24,13 @@ class HomePlaceholderViewCell: UITableViewCell { private let placeholderImageView = UIImageView().then { $0.image = .init(.image(.v2(.placeholder_home))) + $0.contentMode = .scaleAspectFit } private let placeholderMessageLabel = UILabel().then { $0.text = Text.message $0.textColor = .som.v2.gray400 $0.typography = .som.v2.body1 - $0.textAlignment = .center } @@ -57,15 +57,15 @@ class HomePlaceholderViewCell: UITableViewCell { self.contentView.addSubview(self.placeholderImageView) self.placeholderImageView.snp.makeConstraints { - let offset = UIScreen.main.bounds.height * 0.2 + let offset = UIScreen.main.bounds.height * 0.1 $0.top.equalToSuperview().offset(offset) $0.centerX.equalToSuperview() + $0.height.equalTo(113) } self.contentView.addSubview(self.placeholderMessageLabel) self.placeholderMessageLabel.snp.makeConstraints { $0.top.equalTo(self.placeholderImageView.snp.bottom).offset(20) - $0.bottom.equalToSuperview() $0.centerX.equalToSuperview() } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift index 86c810dc..c757c718 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift @@ -139,15 +139,16 @@ class DetailViewCell: UICollectionViewCell { private func setupConstraints() { + self.backgroundColor = .som.v2.white + self.contentView.addSubview(self.memberInfoView) self.memberInfoView.snp.makeConstraints { - $0.top.equalToSuperview().offset(8) - $0.horizontalEdges.equalToSuperview() + $0.top.horizontalEdges.equalToSuperview() } self.contentView.addSubview(self.backgroundImageView) self.backgroundImageView.snp.makeConstraints { - $0.top.equalTo(self.memberInfoView.snp.bottom).offset(8) + $0.top.equalTo(self.memberInfoView.snp.bottom) $0.centerX.equalToSuperview() let size: CGFloat = UIScreen.main.bounds.width - 16 * 2 $0.size.equalTo(size) @@ -188,7 +189,7 @@ class DetailViewCell: UICollectionViewCell { self.contentView.addSubview(self.tags) self.tags.snp.makeConstraints { - $0.bottom.equalTo(backgroundImageView.snp.bottom).offset(-16) + $0.bottom.equalTo(self.backgroundImageView.snp.bottom).offset(-16) $0.leading.equalToSuperview().offset(16) $0.trailing.equalToSuperview().offset(-16) $0.height.equalTo(28) @@ -196,13 +197,13 @@ class DetailViewCell: UICollectionViewCell { self.contentView.addSubview(self.likeAndCommentView) self.likeAndCommentView.snp.makeConstraints { - $0.top.equalTo(self.backgroundImageView.snp.bottom).offset(12) + $0.top.equalTo(self.backgroundImageView.snp.bottom) $0.bottom.horizontalEdges.equalToSuperview() } self.contentView.addSubview(self.deletedCardInDetailBackgroundView) self.deletedCardInDetailBackgroundView.snp.makeConstraints { - $0.top.equalTo(self.memberInfoView.snp.bottom).offset(8) + $0.top.equalTo(self.memberInfoView.snp.bottom) $0.leading.equalToSuperview().offset(16) $0.bottom.trailing.equalToSuperview().offset(-16) } @@ -301,6 +302,7 @@ class DetailViewCell: UICollectionViewCell { self.memberInfoView.updateViewsWhenDeleted() self.likeAndCommentView.updateViewsWhenDeleted() self.backgroundImageView.removeFromSuperview() + self.tags.removeFromSuperview() self.deletedCardInDetailBackgroundView.isHidden = false } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift index 6636423d..cb916734 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift @@ -17,7 +17,7 @@ class DetailViewFooterCell: UICollectionViewCell { // MARK: Views - private let cardView = SOMCard() + private let cardView = SOMCard(type: .comment) // MARK: Initialize diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift index 8cc6b80d..4fc50c0a 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift @@ -34,6 +34,7 @@ class DetailViewController: BaseNavigationViewController, View { static let bottomToastEntryName: String = "bottomToastEntryName" static let blockButtonFloatActionTitle: String = "차단하기" + static let unblockButtonFloatActionTitle: String = "차단해제" static let reportButtonFloatActionTitle: String = "신고하기" static let deleteButtonFloatActionTitle: String = "삭제" @@ -76,12 +77,14 @@ class DetailViewController: BaseNavigationViewController, View { frame: .zero, collectionViewLayout: self.flowLayout ).then { - $0.backgroundColor = .som.v2.white + $0.backgroundColor = .som.v2.gray100 $0.alwaysBounceVertical = true $0.showsVerticalScrollIndicator = false $0.showsHorizontalScrollIndicator = false + $0.contentInsetAdjustmentBehavior = .never + $0.refreshControl = SOMRefreshControl() $0.register(DetailViewCell.self, forCellWithReuseIdentifier: "cell") @@ -110,6 +113,8 @@ class DetailViewController: BaseNavigationViewController, View { private var currentOffset: CGFloat = 0 private var isRefreshEnabled: Bool = true private var shouldRefreshing: Bool = false + + private var actions: [SOMBottomFloatView.FloatAction] = [] // MARK: Override func @@ -119,8 +124,15 @@ class DetailViewController: BaseNavigationViewController, View { NotificationCenter.default.addObserver( self, - selector: #selector(self.reloadData(_:)), - name: .reloadData, + selector: #selector(self.reloadCommentsData(_:)), + name: .reloadCommentsData, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(self.updatedReportState(_:)), + name: .updatedReportState, object: nil ) } @@ -128,9 +140,13 @@ class DetailViewController: BaseNavigationViewController, View { override func setupNaviBar() { super.setupNaviBar() - self.navigationBar.title = self.reactor?.detailType == .feed ? Text.feedDetailNavigationTitle : Text.commentDetailNavigationTitle + guard let reactor = self.reactor else { return } + + self.navigationBar.title = reactor.entranceCardType == .feed ? + Text.feedDetailNavigationTitle : + Text.commentDetailNavigationTitle - if self.reactor?.detailType == .comment { + if reactor.entranceCardType == .comment { self.navigationBar.setLeftButtons([self.leftHomeButton]) } self.navigationBar.setRightButtons([self.rightMoreButton]) @@ -158,73 +174,93 @@ class DetailViewController: BaseNavigationViewController, View { } } - override func bind() { - super.bind() + + // MARK: - Bind + + func bind(reactor: DetailViewReactor) { - // Navigation pop to root - self.leftHomeButton.rx.throttleTap + // 답카드 작성 전환 + self.floatingButton.backgoundButton.rx.throttleTap .subscribe(with: self) { object, _ in - if let navigationController = object.navigationController { - navigationController.popToRootViewController(animated: false) - } else { - object.navigationPop(animated: false) - } + let writeCardViewController = WriteCardViewController() + writeCardViewController.reactor = reactor.reactorForWriteCard() + object.navigationPush(writeCardViewController, animated: true, bottomBarHidden: true) } .disposed(by: self.disposeBag) - self.rightMoreButton.rx.throttleTap + let detailCard = reactor.state.map(\.detailCard).filterNil().distinctUntilChanged() + let isBlocked = reactor.state.map(\.isBlocked).distinctUntilChanged() + let isReported = reactor.state.map(\.isReported).distinctUntilChanged() + + let rightMoreButtonDidTap = self.rightMoreButton.rx.throttleTap.share() + // 더보기 버튼 액션 + rightMoreButtonDidTap + .withLatestFrom(detailCard) + .filter { $0.isOwnCard } .subscribe(with: self) { object, _ in - var actions: [SOMBottomFloatView.FloatAction] { - - if object.detailCard.isOwnCard { - - return [ - .init( - title: Text.deleteButtonFloatActionTitle, - image: .init(.icon(.v2(.outlined(.trash)))), - foregroundColor: .som.v2.rMain, - action: { [weak object] in - SwiftEntryKit.dismiss(.specific(entryName: Text.bottomFloatEntryName)) { - - object?.showDeleteCardDialog() - } - } - ) - ] - } else { - - return [ - .init( - title: Text.blockButtonFloatActionTitle, - image: .init(.icon(.v2(.outlined(.hide)))), - action: { [weak object] in - SwiftEntryKit.dismiss(.specific(entryName: Text.bottomFloatEntryName)) { - - object?.showBlockedUserDialog() - } - } - ), - .init( - title: Text.reportButtonFloatActionTitle, - image: .init(.icon(.v2(.outlined(.flag)))), - foregroundColor: .som.v2.rMain, - action: { [weak object] in - guard let object = object, let reactor = object.reactor else { return } - - SwiftEntryKit.dismiss(.specific(entryName: Text.bottomFloatEntryName)) { - - let reportViewController = ReportViewController() - reportViewController.reactor = reactor.reactorForReport() - object.navigationPush(reportViewController, animated: true, bottomBarHidden: true) - } + object.actions = [ + .init( + title: Text.deleteButtonFloatActionTitle, + image: .init(.icon(.v2(.outlined(.trash)))), + foregroundColor: .som.v2.rMain, + action: { [weak object] in + SwiftEntryKit.dismiss(.specific(entryName: Text.bottomFloatEntryName)) { + + object?.showDeleteCardDialog() + } + } + ) + ] + + let bottomFloatView = SOMBottomFloatView(actions: object.actions) + + var wrapper: SwiftEntryKitViewWrapper = bottomFloatView.sek + wrapper.entryName = Text.bottomFloatEntryName + wrapper.showBottomFloat(screenInteraction: .dismiss) + } + .disposed(by: self.disposeBag) + + rightMoreButtonDidTap + .withLatestFrom(Observable.combineLatest(detailCard, isBlocked, isReported)) + .filter { $0.0.isOwnCard == false } + .map { ($0.1, $0.2) } + .subscribe(with: self) { object, combined in + + let (isBlocked, isReported) = combined + + object.actions = [ + .init( + title: isBlocked ? Text.blockButtonFloatActionTitle : Text.unblockButtonFloatActionTitle, + image: .init(.icon(.v2(.outlined(isBlocked ? .hide : .eye)))), + action: { [weak object] in + SwiftEntryKit.dismiss(.specific(entryName: Text.bottomFloatEntryName)) { + if isBlocked { + object?.showBlockedUserDialog() + } else { + reactor.action.onNext(.block(isBlocked: false)) } - ) - ] - } - } + } + } + ), + .init( + title: Text.reportButtonFloatActionTitle, + image: .init(.icon(.v2(.outlined(.flag)))), + foregroundColor: .som.v2.rMain, + isEnabled: isReported == false, + action: { [weak object] in + + SwiftEntryKit.dismiss(.specific(entryName: Text.bottomFloatEntryName)) { + + let reportViewController = ReportViewController() + reportViewController.reactor = reactor.reactorForReport() + object?.navigationPush(reportViewController, animated: true, bottomBarHidden: true) + } + } + ) + ] - let bottomFloatView = SOMBottomFloatView(actions: actions) + let bottomFloatView = SOMBottomFloatView(actions: object.actions) var wrapper: SwiftEntryKitViewWrapper = bottomFloatView.sek wrapper.entryName = Text.bottomFloatEntryName @@ -232,28 +268,17 @@ class DetailViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) + // 카드 삭제 후 X 버튼 액션 self.rightDeleteButton.rx.throttleTap .subscribe(with: self) { object, _ in - if let navigationController = object.navigationController { - navigationController.popToRootViewController(animated: false) - } else { - object.navigationPop(animated: false) - } + object.navigationPop(to: HomeViewController.self, animated: false) } .disposed(by: self.disposeBag) - } - - - // MARK: - Bind - - func bind(reactor: DetailViewReactor) { - // 답카드 작성 전환 - self.floatingButton.backgoundButton.rx.throttleTap + // 답카드 홈 버튼 액션 + self.leftHomeButton.rx.throttleTap .subscribe(with: self) { object, _ in - let writeCardViewController = WriteCardViewController() - writeCardViewController.reactor = reactor.reactorForWriteCard() - object.navigationPush(writeCardViewController, animated: true, bottomBarHidden: true) + object.navigationPop(to: HomeViewController.self, animated: false) } .disposed(by: self.disposeBag) @@ -278,13 +303,11 @@ class DetailViewController: BaseNavigationViewController, View { .observe(on: MainScheduler.asyncInstance) .filter { $0 == false } .subscribe(with: self.collectionView) { collectionView, _ in - collectionView.refreshControl?.endRefreshingWithOffset() + collectionView.refreshControl?.endRefreshing() } .disposed(by: self.disposeBag) - reactor.state.map(\.detailCard) - .filterNil() - .distinctUntilChanged() + detailCard .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, detailCard in object.detailCard = detailCard @@ -308,19 +331,36 @@ class DetailViewController: BaseNavigationViewController, View { object.collectionView.reloadData() } } - .disposed(by: disposeBag) + .disposed(by: self.disposeBag) reactor.state.map(\.isLiked) .distinctUntilChanged() .filter { $0 } + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in - NotificationCenter.default.post(name: .reloadData, object: object) + + let updated: DetailCardInfo + if object.detailCard.isLike { + + let updatedLikeCnt = object.detailCard.likeCnt - 1 + updated = object.detailCard.updateLikeCnt(updatedLikeCnt, with: false) + } else { + + let updatedLikeCnt = object.detailCard.likeCnt + 1 + updated = object.detailCard.updateLikeCnt(updatedLikeCnt, with: true) + } + + object.detailCard = updated + + UIView.performWithoutAnimation { + object.collectionView.reloadData() + } } .disposed(by: self.disposeBag) - reactor.state.map(\.isBlocked) - .distinctUntilChanged() - .filter { $0 } + isBlocked + .filter { $0 == false } + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in let title = Text.blockToastLeadingTitle + object.detailCard.nickname + Text.blockToastTrailingTitle @@ -340,10 +380,15 @@ class DetailViewController: BaseNavigationViewController, View { .disposed(by: self.disposeBag) reactor.state.map(\.isDeleted) - .filterNil() .distinctUntilChanged() .filter { $0 } + .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, _ in + NotificationCenter.default.post(name: .reloadData, object: nil, userInfo: nil) + if reactor.entranceCardType == .comment { + NotificationCenter.default.post(name: .reloadCommentsData, object: nil, userInfo: nil) + } + object.navigationBar.title = Text.deletedNavigationTitle object.navigationBar.setRightButtons([object.rightDeleteButton]) @@ -357,37 +402,43 @@ class DetailViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) - reactor.state.map(\.hasErrors) - .filterNil() - .distinctUntilChanged() - .subscribe(with: self) { object, hasErrors in - - switch reactor.entranceType { - case .navi: - object.isDeleted = true - - UIView.performWithoutAnimation { - object.collectionView.reloadData() - } - case .push: - return - // let notificationTabBarController = NotificationTabBarController() - // notificationTabBarController.reactor = reactor.reactorForNoti() - // - // object.navigationPush(notificationTabBarController, animated: false) - // object.navigationController?.viewControllers.removeAll(where: { $0.isKind(of: DetailViewController.self) }) - } - } - .disposed(by: self.disposeBag) + // reactor.state.map(\.hasErrors) + // .filterNil() + // .distinctUntilChanged() + // .subscribe(with: self) { object, hasErrors in + // + // switch reactor.entranceType { + // case .navi: + // object.isDeleted = true + // + // UIView.performWithoutAnimation { + // object.collectionView.reloadData() + // } + // case .push: + // return + // let notificationTabBarController = NotificationTabBarController() + // notificationTabBarController.reactor = reactor.reactorForNoti() + // + // object.navigationPush(notificationTabBarController, animated: false) + // object.navigationController?.viewControllers.removeAll(where: { $0.isKind(of: DetailViewController.self) }) + // } + // } + // .disposed(by: self.disposeBag) } // MARK: Objc func @objc - private func reloadData(_ notification: Notification) { + private func reloadCommentsData(_ notification: Notification) { + + self.reactor?.action.onNext(.refreshForComment) + } + + @objc + private func updatedReportState(_ notification: Notification) { - self.reactor?.action.onNext(.landing) + self.reactor?.action.onNext(.updateReport(true)) } } @@ -418,7 +469,7 @@ extension DetailViewController: UICollectionViewDataSource { guard let reactor = self.reactor else { return cell } - cell.likeAndCommentView.likeBackgroundButton.rx.throttleTap + cell.likeAndCommentView.likeBackgroundButton.rx.throttleTap(.seconds(3)) .withLatestFrom(reactor.state.compactMap(\.detailCard).map(\.isLike)) .subscribe(onNext: { isLike in reactor.action.onNext(.updateLike(isLike == false)) @@ -443,8 +494,9 @@ extension DetailViewController: UICollectionViewDataSource { object.navigationPop() } else { /// 없다면 새로운 viewController로 naviPush + guard let prevCardId = object.detailCard.prevCardId else { return } let detailViewController = DetailViewController() - detailViewController.reactor = reactor.reactorForPush(object.detailCard.id) + detailViewController.reactor = reactor.reactorForPush(prevCardId) object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) } } @@ -568,7 +620,7 @@ extension DetailViewController: UICollectionViewDelegateFlowLayout { if self.shouldRefreshing { self.collectionView.refreshControl?.beginRefreshingWithOffset( - self.detailCard.storyExpirationTime == nil ? 0 : 30 + self.detailCard.storyExpirationTime == nil ? 0 : 23 ) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift index b60496fb..c961bc83 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift @@ -15,18 +15,15 @@ class DetailViewReactor: Reactor { case navi } - enum DetailType { - case feed - case comment - } - enum Action: Equatable { case landing + case refreshForComment case refresh case moreFindForComment(lastId: String) case delete case block(isBlocked: Bool) case updateLike(Bool) + case updateReport(Bool) } enum Mutation { @@ -35,8 +32,9 @@ class DetailViewReactor: Reactor { case moreComment([BaseCardInfo]) case updateIsRefreshing(Bool) case updateIsLiked(Bool) + case updateIsDeleted(Bool) + case updateReported(Bool) case updateIsBlocked(Bool) - case updateIsDeleted(Bool?) case updateErrors(Int?) } @@ -45,8 +43,9 @@ class DetailViewReactor: Reactor { fileprivate(set) var commentCards: [BaseCardInfo] fileprivate(set) var isRefreshing: Bool fileprivate(set) var isLiked: Bool + fileprivate(set) var isDeleted: Bool + fileprivate(set) var isReported: Bool fileprivate(set) var isBlocked: Bool - fileprivate(set) var isDeleted: Bool? fileprivate(set) var hasErrors: Int? } @@ -55,8 +54,9 @@ class DetailViewReactor: Reactor { commentCards: [], isRefreshing: false, isLiked: false, - isBlocked: false, - isDeleted: nil, + isDeleted: false, + isReported: false, + isBlocked: true, hasErrors: nil ) @@ -65,13 +65,13 @@ class DetailViewReactor: Reactor { private let locationManager: LocationManagerDelegate - let detailType: DetailType + let entranceCardType: EntranceCardType let entranceType: EntranceType let selectedCardId: String init( dependencies: AppDIContainerable, - _ detailType: DetailType, + _ entranceCardType: EntranceCardType, type entranceType: EntranceType = .navi, with selectedCardId: String ) { @@ -80,7 +80,7 @@ class DetailViewReactor: Reactor { self.locationManager = dependencies.rootContainer.resolve(ManagerProviderType.self).locationManager - self.detailType = detailType + self.entranceCardType = entranceCardType self.entranceType = entranceType self.selectedCardId = selectedCardId } @@ -90,14 +90,19 @@ class DetailViewReactor: Reactor { case .landing: return .concat([ - self.detailCard(), + self.detailCard() + .catch(self.catchClosure), self.commentCards() ]) + case .refreshForComment: + + return self.commentCards() case .refresh: return .concat([ .just(.updateIsRefreshing(true)), - self.detailCard(), + self.detailCard() + .catch(self.catchClosure), self.commentCards(), .just(.updateIsRefreshing(false)) ]) @@ -113,7 +118,11 @@ class DetailViewReactor: Reactor { guard let memberId = self.currentState.detailCard?.memberId else { return .empty() } return self.cardUseCase.updateBlocked(id: memberId, isBlocked: isBlocked) - .map(Mutation.updateIsBlocked) + .flatMapLatest { isBlockedSuccess -> Observable in + /// isBlocked == true 일 때, 차단 요청 + return isBlockedSuccess ? .just(.updateIsBlocked(isBlocked == false)) : .empty() + } + .catch(self.catchClosure) case let .updateLike(isLike): return .concat([ @@ -122,12 +131,13 @@ class DetailViewReactor: Reactor { .filter { $0 } .withUnretained(self) .flatMapLatest { object, _ -> Observable in - return .concat([ - object.detailCard(), - .just(.updateIsLiked(true)) - ]) + return .just(.updateIsLiked(true)) } + .catch(self.catchClosure) ]) + case let .updateReport(isReported): + + return .just(.updateReported(isReported)) } } @@ -144,10 +154,12 @@ class DetailViewReactor: Reactor { newState.isRefreshing = isRefreshing case let .updateIsLiked(isLiked): newState.isLiked = isLiked - case let .updateIsBlocked(isBlocked): - newState.isBlocked = isBlocked case let .updateIsDeleted(isDeleted): newState.isDeleted = isDeleted + case let .updateReported(isReported): + newState.isReported = isReported + case let .updateIsBlocked(isBlocked): + newState.isBlocked = isBlocked case let .updateErrors(hasErrors): newState.hasErrors = hasErrors } @@ -235,12 +247,22 @@ extension DetailViewReactor { return { error in let nsError = error as NSError - return .concat([ - .just(.updateIsBlocked(false)), - .just(.updateIsDeleted(false)), - .just(.updateIsRefreshing(false)), - .just(.updateErrors(nsError.code)) - ]) + // errorCode == 409 일 때, 해당 사용자 중복 차단 + if case 409 = nsError.code { + return .concat([ + .just(.updateIsRefreshing(false)), + .just(.updateIsBlocked(false)) + ]) + } + + if case 410 = nsError.code { + return .concat([ + .just(.updateIsRefreshing(false)), + .just(.updateIsDeleted(true)) + ]) + } + + return .empty() } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift index 3435be38..8e3c8a54 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewController.swift @@ -137,6 +137,15 @@ class ReportViewController: BaseNavigationViewController, View { object.showSuccessReportedDialog() } .disposed(by: self.disposeBag) + + reactor.state.map(\.hasErrors) + .filter { $0 } + .subscribe(with: self) { object, _ in + object.navigationPop { + NotificationCenter.default.post(name: .updatedReportState, object: nil, userInfo: nil) + } + } + .disposed(by: self.disposeBag) } } @@ -182,7 +191,9 @@ private extension ReportViewController { style: .primary, action: { UIApplication.topViewController?.dismiss(animated: true) { - self.navigationPop() + self.navigationPop { + NotificationCenter.default.post(name: .updatedReportState, object: nil, userInfo: nil) + } } } ) diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift index f61876b7..c6808266 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Report/ReportViewReactor.swift @@ -18,11 +18,13 @@ class ReportViewReactor: Reactor { case updateReportReason(ReportType?) /// 업로드 완료 여부 변경 case updateisReported(Bool) + case updateHasErrors(Bool) } struct State { fileprivate(set) var reportReason: ReportType? fileprivate(set) var isReported: Bool + fileprivate(set) var hasErrors: Bool } var initialState: State @@ -37,7 +39,7 @@ class ReportViewReactor: Reactor { self.cardUseCase = dependencies.rootContainer.resolve(CardUseCase.self) self.id = id - self.initialState = State(reportReason: nil, isReported: false) + self.initialState = State(reportReason: nil, isReported: false, hasErrors: false) } func mutate(action: Action) -> Observable { @@ -51,6 +53,7 @@ class ReportViewReactor: Reactor { return self.cardUseCase.reportCard(id: self.id, reportType: reportReason.rawValue) .map(Mutation.updateisReported) + .catch(self.catchClosure) } } @@ -61,7 +64,23 @@ class ReportViewReactor: Reactor { newState.reportReason = reportReason case let .updateisReported(isReported): newState.isReported = isReported + case let .updateHasErrors(hasErrors): + newState.hasErrors = hasErrors } return newState } } + +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() + } + } + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/LikeAndCommentView.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/LikeAndCommentView.swift index 424952c1..b6d18655 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/LikeAndCommentView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/LikeAndCommentView.swift @@ -20,35 +20,25 @@ class LikeAndCommentView: UIView { // MARK: Views let likeBackgroundButton = UIButton() - private let likeContainer = UIStackView().then { - $0.axis = .horizontal - $0.alignment = .center - $0.distribution = .equalSpacing - $0.spacing = 4 - } + private let likeContainer = UIView() private let likeImageView = UIImageView().then { $0.image = .init(.icon(.v2(.outlined(.heart)))) $0.tintColor = .som.v2.gray500 } private let likeCountLabel = UILabel().then { $0.textColor = .som.v2.gray500 - $0.typography = .som.v2.caption1 + $0.typography = .som.v2.caption1.withAlignment(.left) } let commentBackgroundButton = UIButton() - private let commentContainer = UIStackView().then { - $0.axis = .horizontal - $0.alignment = .center - $0.distribution = .equalSpacing - $0.spacing = 4 - } + private let commentContainer = UIView() private let commentImageView = UIImageView().then { $0.image = .init(.icon(.v2(.outlined(.message_circle)))) $0.tintColor = .som.v2.gray500 } private let commentCountLabel = UILabel().then { $0.textColor = .som.v2.gray500 - $0.typography = .som.v2.caption1 + $0.typography = .som.v2.caption1.withAlignment(.left) } private let visitedLabel = UILabel().then { @@ -101,47 +91,60 @@ class LikeAndCommentView: UIView { private func setupConstraints() { + self.backgroundColor = .som.v2.white + self.snp.makeConstraints { $0.height.equalTo(44) } - let container = UIStackView(arrangedSubviews: [ - self.likeContainer, - self.commentContainer - ]).then { - $0.axis = .horizontal - $0.alignment = .center - $0.distribution = .equalSpacing - $0.spacing = 15 - } - self.addSubview(container) - container.snp.makeConstraints { - $0.centerY.equalToSuperview() + self.addSubview(self.likeContainer) + self.likeContainer.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() $0.leading.equalToSuperview().offset(16) + $0.width.equalTo(60) } - - self.likeContainer.addArrangedSubviews(self.likeImageView, self.likeCountLabel) + self.likeContainer.addSubview(self.likeImageView) self.likeImageView.snp.makeConstraints { + $0.centerY.leading.equalToSuperview() $0.size.equalTo(20) } + self.likeContainer.addSubview(self.likeCountLabel) + self.likeCountLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalTo(self.likeImageView.snp.trailing).offset(4) + } + self.addSubview(self.likeBackgroundButton) self.likeBackgroundButton.snp.makeConstraints { - $0.edges.equalTo(self.likeContainer) + $0.edges.equalTo(self.likeImageView) } - self.commentContainer.addArrangedSubviews(self.commentImageView, self.commentCountLabel) + self.addSubview(self.commentContainer) + self.commentContainer.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.leading.equalTo(self.likeContainer.snp.trailing) + $0.width.equalTo(60) + } + self.commentContainer.addSubview(self.commentImageView) self.commentImageView.snp.makeConstraints { + $0.centerY.leading.equalToSuperview() $0.size.equalTo(20) } + self.commentContainer.addSubview(self.commentCountLabel) + self.commentCountLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalTo(self.commentImageView.snp.trailing).offset(4) + } + self.addSubview(self.commentBackgroundButton) self.commentBackgroundButton.snp.makeConstraints { - $0.edges.equalTo(self.commentContainer) + $0.edges.equalTo(self.commentImageView) } self.addSubview(self.visitedLabel) self.visitedLabel.snp.makeConstraints { $0.centerY.equalToSuperview() - $0.leading.greaterThanOrEqualTo(container.snp.trailing).offset(20) + $0.leading.greaterThanOrEqualTo(self.commentContainer.snp.trailing).offset(20) $0.trailing.equalToSuperview().offset(-20) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift index ce394814..e9e2ba95 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/MemberInfoView.swift @@ -22,6 +22,9 @@ class MemberInfoView: UIView { /// 상세보기, 멤버 이미지 // let memberBackgroundButton = UIButton() private let memberImageView = UIImageView().then { + $0.backgroundColor = .som.v2.gray300 + $0.layer.borderColor = UIColor.som.v2.gray300.cgColor + $0.layer.borderWidth = 1 $0.layer.cornerRadius = 36 * 0.5 $0.clipsToBounds = true } @@ -109,6 +112,8 @@ class MemberInfoView: UIView { private func setupConstraints() { + self.backgroundColor = .som.v2.white + self.snp.makeConstraints { $0.height.equalTo(52) } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/WrittenTags/WrittenTags.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/WrittenTags/WrittenTags.swift index 479ef32a..c9f26e11 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/WrittenTags/WrittenTags.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Views/WrittenTags/WrittenTags.swift @@ -30,7 +30,7 @@ class WrittenTags: UIView { collectionViewLayout: UICollectionViewFlowLayout().then { $0.scrollDirection = .horizontal $0.minimumInteritemSpacing = 6 - $0.minimumLineSpacing = 0 + $0.minimumLineSpacing = 6 } ).then { $0.backgroundColor = .clear diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift index 7ddcb7ba..666811e2 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift @@ -47,7 +47,7 @@ class NotificationViewController: BaseNavigationViewController, View { $0.delegate = self } - private lazy var tableView = UITableView(frame: .zero, style: .plain).then { + private lazy var tableView = UITableView(frame: .zero, style: .grouped).then { $0.backgroundColor = .som.v2.white $0.indicatorStyle = .black $0.separatorStyle = .none @@ -116,11 +116,6 @@ class NotificationViewController: BaseNavigationViewController, View { private var shouldRefreshing: Bool = false - // MARK: Variables + Rx - - private let willPushTypeAndCardId = PublishRelay<(detailType: DetailViewReactor.DetailType, id: String)?>() - - // MARK: Override func override func setupNaviBar() { @@ -150,19 +145,6 @@ class NotificationViewController: BaseNavigationViewController, View { func bind(reactor: NotificationViewReactor) { - self.willPushTypeAndCardId - .filterNil() - .distinctUntilChanged({ $0.detailType == $1.detailType && $0.id == $1.id }) - .subscribe(with: self) { object, detailInfo in - let detailViewController = DetailViewController() - detailViewController.reactor = reactor.reactorForDetail( - detailType: detailInfo.detailType, - with: detailInfo.id - ) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) - } - .disposed(by: self.disposeBag) - // Action self.rx.viewDidLoad .map { _ in Reactor.Action.landing } @@ -195,6 +177,19 @@ class NotificationViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) + reactor.state.map(\.pushInfo) + .filterNil() + .distinctUntilChanged(reactor.canUpdatePushInfos) + .subscribe(with: self) { object, pushInfo in + let detailViewController = DetailViewController() + detailViewController.reactor = reactor.reactorForDetail( + entranceType: pushInfo.entranceType, + with: pushInfo.id + ) + object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) + } + .disposed(by: self.disposeBag) + reactor.state.map { NotificationViewReactor.DisplayStates( displayType: $0.displayType, @@ -243,6 +238,15 @@ class NotificationViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) } + + + // MARK: Objc func + + @objc + private func reloadData(_ notification: Notification) { + + self.reactor?.action.onNext(.landing) + } } @@ -302,49 +306,107 @@ extension NotificationViewController: UITableViewDelegate { } case let .unread(notification): - var detailInfo: (detailType: DetailViewReactor.DetailType, id: String)? { + var pushOrRequestReadInfo: NotificationViewReactor.PushOrRequestReadInfo? { switch notification { case let .default(notification): + if case .feedLike = notification.notificationInfo.notificationType { - return (.feed, notification.targetCardId) + return .init( + entranceType: .feed, + notificationId: notification.notificationInfo.notificationId, + targetCardId: notification.targetCardId, + shouldRead: true + ) } if case .commentLike = notification.notificationInfo.notificationType { - return (.comment, notification.targetCardId) + return .init( + entranceType: .comment, + notificationId: notification.notificationInfo.notificationId, + targetCardId: notification.targetCardId, + shouldRead: true + ) } if case .commentWrite = notification.notificationInfo.notificationType { - return (.comment, notification.targetCardId) + return .init( + entranceType: .comment, + notificationId: notification.notificationInfo.notificationId, + targetCardId: notification.targetCardId, + shouldRead: true + ) } return nil - default: - return nil + /// follow, deleted, blocked 는 읽기 API만 호출 + case let .follow(notification): + + return .init( + entranceType: .feed, + notificationId: notification.notificationInfo.notificationId, + targetCardId: nil, + shouldRead: true + ) + case let .deleted(notification): + + return .init( + entranceType: .feed, + notificationId: notification.notificationInfo.notificationId, + targetCardId: nil, + shouldRead: true + ) + case let .blocked(notification): + + return .init( + entranceType: .feed, + notificationId: notification.notificationInfo.notificationId, + targetCardId: nil, + shouldRead: true + ) } } - guard let detailInfo = detailInfo else { return } + guard let pushOrRequestReadInfo = pushOrRequestReadInfo else { return } - self.willPushTypeAndCardId.accept(detailInfo) + self.reactor?.action.onNext(.updatePushOrRequestReadInfo(pushOrRequestReadInfo)) case let .read(notification): - var detailInfo: (detailType: DetailViewReactor.DetailType, id: String)? { + var pushOrRequestReadInfo: NotificationViewReactor.PushOrRequestReadInfo? { switch notification { case let .default(notification): + if case .feedLike = notification.notificationInfo.notificationType { - return (.feed, notification.targetCardId) + return .init( + entranceType: .feed, + notificationId: notification.notificationInfo.notificationId, + targetCardId: notification.targetCardId, + shouldRead: false + ) } if case .commentLike = notification.notificationInfo.notificationType { - return (.comment, notification.targetCardId) + return .init( + entranceType: .comment, + notificationId: notification.notificationInfo.notificationId, + targetCardId: notification.targetCardId, + shouldRead: false + ) } if case .commentWrite = notification.notificationInfo.notificationType { - return (.comment, notification.targetCardId) + return .init( + entranceType: .comment, + notificationId: notification.notificationInfo.notificationId, + targetCardId: notification.targetCardId, + shouldRead: false + ) } return nil default: + return nil } } - guard let detailInfo = detailInfo else { return } + guard let pushOrRequestReadInfo = pushOrRequestReadInfo else { return } + + self.reactor?.action.onNext(.updatePushOrRequestReadInfo(pushOrRequestReadInfo)) - self.willPushTypeAndCardId.accept(detailInfo) default: + return } } @@ -400,6 +462,15 @@ extension NotificationViewController: UITableViewDelegate { } } + // group style이기 때문에 footer 제거 + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return nil + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return 0 + } + func tableView( _ tableView: UITableView, willDisplay cell: UITableViewCell, diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift index d42783b6..a718751a 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift @@ -11,29 +11,13 @@ import Alamofire class NotificationViewReactor: Reactor { - struct DisplayStates { - let displayType: DisplayType - let unreads: [CompositeNotificationInfo]? - let reads: [CompositeNotificationInfo]? - let notices: [NoticeInfo]? - } - - enum DisplayType: Equatable { - enum ActivityType: Equatable { - case unread - case read - } - - case activity(ActivityType) - case notice - } - enum Action: Equatable { case landing case refresh case updateDisplayType(DisplayType) case moreFind(lastId: String, displayType: DisplayType) case requestRead(String) + case updatePushOrRequestReadInfo(PushOrRequestReadInfo) } enum Mutation { @@ -42,6 +26,7 @@ class NotificationViewReactor: Reactor { case notices([NoticeInfo]) case moreNotices([NoticeInfo]) case updateDisplayType(DisplayType) + case updatePushOrRequestReadInfo((entranceType: EntranceCardType, id: String)?) case updateIsRefreshing(Bool) case updateIsReadSuccess(Bool) } @@ -51,6 +36,7 @@ class NotificationViewReactor: Reactor { fileprivate(set) var notificationsForUnread: [CompositeNotificationInfo]? fileprivate(set) var notifications: [CompositeNotificationInfo]? fileprivate(set) var notices: [NoticeInfo]? + fileprivate(set) var pushInfo: (entranceType: EntranceCardType, id: String)? fileprivate(set) var isRefreshing: Bool fileprivate(set) var isReadSuccess: Bool } @@ -69,6 +55,7 @@ class NotificationViewReactor: Reactor { notificationsForUnread: nil, notifications: nil, notices: nil, + pushInfo: nil, isRefreshing: false, isReadSuccess: false ) @@ -135,6 +122,48 @@ class NotificationViewReactor: Reactor { return self.notificationUseCase.requestRead(notificationId: selectedId) .map(Mutation.updateIsReadSuccess) + + case let .updatePushOrRequestReadInfo(pushOrRequestReadInfo): + + /// 읽은 알림 여부 확인 + if pushOrRequestReadInfo.shouldRead { + /// 읽어야 하는 알림일 경우, 읽음 API 호출 + return self.notificationUseCase.requestRead(notificationId: pushOrRequestReadInfo.notificationId) + .withUnretained(self) + .flatMapLatest { object, _ -> Observable in + /// 알림 화면 리로드 + return Observable.zip( + object.notificationUseCase.unreadNotifications(lastId: nil), + object.notificationUseCase.readNotifications(lastId: nil) + ) + .flatMapLatest { unreads, reads -> Observable in + + if let targetCardId = pushOrRequestReadInfo.targetCardId { + + return .concat([ + .just(.notifications(unreads: unreads, reads: reads)), + .just(.updatePushOrRequestReadInfo(nil)), + .just(.updatePushOrRequestReadInfo((pushOrRequestReadInfo.entranceType, targetCardId))) + ]) + } else { + return .concat([ + .just(.notifications(unreads: unreads, reads: reads)), + .just(.updatePushOrRequestReadInfo(nil)) + ]) + } + } + } + } else { + + if let targetCardId = pushOrRequestReadInfo.targetCardId { + return .concat([ + .just(.updatePushOrRequestReadInfo(nil)), + .just(.updatePushOrRequestReadInfo((pushOrRequestReadInfo.entranceType, targetCardId))) + ]) + } else { + return .just(.updatePushOrRequestReadInfo(nil)) + } + } } } @@ -153,6 +182,8 @@ class NotificationViewReactor: Reactor { newState.notices? += notices case let .updateDisplayType(displayType): newState.displayType = displayType + case let .updatePushOrRequestReadInfo(pushInfo): + newState.pushInfo = pushInfo case let .updateIsRefreshing(isRefreshing): newState.isRefreshing = isRefreshing case let .updateIsReadSuccess(isReadSuccess): @@ -180,6 +211,33 @@ private extension NotificationViewReactor { } } +extension NotificationViewReactor { + + struct DisplayStates { + let displayType: DisplayType + let unreads: [CompositeNotificationInfo]? + let reads: [CompositeNotificationInfo]? + let notices: [NoticeInfo]? + } + + enum DisplayType: Equatable { + enum ActivityType: Equatable { + case unread + case read + } + + case activity(ActivityType) + case notice + } + + struct PushOrRequestReadInfo: Equatable { + let entranceType: EntranceCardType + let notificationId: String + let targetCardId: String? + let shouldRead: Bool + } +} + extension NotificationViewReactor { var catchClosureNotis: ((Error) throws -> Observable ) { @@ -218,6 +276,14 @@ extension NotificationViewReactor { } } + func canUpdatePushInfos( + prev prevPushInfo: (entranceType: EntranceCardType, id: String), + curr currPushInfo: (entranceType: EntranceCardType, id: String) + ) -> Bool { + return prevPushInfo.entranceType == currPushInfo.entranceType && + prevPushInfo.id == currPushInfo.id + } + func canUpdateCells( prev prevStates: DisplayStates, curr currStates: DisplayStates @@ -231,7 +297,7 @@ extension NotificationViewReactor { extension NotificationViewReactor { - func reactorForDetail(detailType: DetailViewReactor.DetailType, with id: String) -> DetailViewReactor { - DetailViewReactor(dependencies: self.dependencies, detailType, type: .navi, with: id) + func reactorForDetail(entranceType: EntranceCardType, with id: String) -> DetailViewReactor { + DetailViewReactor(dependencies: self.dependencies, entranceType, type: .navi, with: id) } } diff --git a/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift b/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift index ae98b23b..c6619121 100644 --- a/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift @@ -114,7 +114,7 @@ extension MainTabBarReactor { } func reactorForWriteCard() -> WriteCardViewReactor { - WriteCardViewReactor(dependencies: self.dependencies, type: .card) + WriteCardViewReactor(dependencies: self.dependencies) } // func reactorForTags() -> TagsViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift index a368ff28..826334c3 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTagFooter.swift @@ -155,7 +155,6 @@ extension WriteCardTagFooter: UITextFieldDelegate { } func textFieldDidEndEditing(_ textField: UITextField) { - self.textField.text = self.placeholder self.imageView.image = .init(.icon(.v2(.outlined(.plus)))) self.imageView.tintColor = .som.v2.white self.delegate?.textFieldDidEndEditing(self) diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTags.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTags.swift index 045e4d7a..6002b8a0 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTags.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/Tags/WriteCardTags.swift @@ -311,13 +311,20 @@ extension WriteCardTags: WriteCardTagDelegate { extension WriteCardTags: WriteCardTagFooterDelegate { func textFieldDidBeginEditing(_ textField: WriteCardTagFooter) { - self.footerText = nil + self.footerText = textField.text self.collectionView.collectionViewLayout.invalidateLayout() self.delegate?.textFieldDidBeginEditing(textField) } func textFieldDidEndEditing(_ textField: WriteCardTagFooter) { - self.footerText = textField.text + if let text = textField.text, text.isEmpty == false { + let addedTag: WriteCardTagModel = .init(originalText: text, typography: self.typography) + var new = self.models + new.append(addedTag) + self.updateWrittenTags.accept(new) + } + textField.text = Text.tagPlaceholder + self.footerText = Text.tagPlaceholder self.collectionView.collectionViewLayout.invalidateLayout() self.scrollToRight(animated: true) } diff --git a/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift b/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift index f87da320..4b914b82 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/Views/TextView/WriteCardTextView.swift @@ -50,7 +50,7 @@ class WriteCardTextView: UIView { $0.textContainerInset = .init(top: 20, left: 24, bottom: 20, right: 24) $0.textContainer.lineFragmentPadding = 0 - $0.scrollIndicatorInsets = .init(top: 4, left: 0, bottom: 4, right: 0) + $0.scrollIndicatorInsets = .init(top: 20, left: 0, bottom: 20, right: 0) $0.indicatorStyle = .white $0.isScrollEnabled = false @@ -188,13 +188,14 @@ class WriteCardTextView: UIView { attributes: attributes ) - let size: CGSize = .init(width: textView.bounds.width, height: .greatestFiniteMagnitude) - let textSize: CGSize = textView.sizeThatFits(size) + /// width 계산 시 textContainerInset 고려 + let textSize: CGSize = .init(width: textView.bounds.width - 24 * 2, height: .greatestFiniteMagnitude) var boundingHeight = attributedText.boundingRect( with: textSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).height + /// 자연스러운 줄바꿈을 위해 offset 추가 boundingHeight += 1.0 let lines: CGFloat = boundingHeight / self.typography.lineHeight diff --git a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift index 796db663..b58314b3 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewController.swift @@ -124,7 +124,7 @@ class WriteCardViewController: BaseNavigationViewController, View { override func setupNaviBar() { super.setupNaviBar() - self.navigationBar.title = self.reactor?.requestType == .card ? Text.navigationTitle : Text.commentNavigationTitle + self.navigationBar.title = self.reactor?.entranceType == .feed ? Text.navigationTitle : Text.commentNavigationTitle self.navigationBar.setRightButtons([self.writeButton]) } @@ -167,6 +167,12 @@ class WriteCardViewController: BaseNavigationViewController, View { } } + override func viewDidLoad() { + super.viewDidLoad() + + PHPhotoLibrary.requestAuthorization(for: .readWrite) { _ in } + } + override func updatedKeyboard(withoutBottomSafeInset height: CGFloat) { super.updatedKeyboard(withoutBottomSafeInset: height) @@ -182,7 +188,7 @@ class WriteCardViewController: BaseNavigationViewController, View { func bind(reactor: WriteCardViewReactor) { var options: [SelectOptionItem.OptionType] { - if reactor.requestType == .card { + if reactor.entranceType == .feed { return [.distanceShare, .story] } else { return [.distanceShare] @@ -429,6 +435,7 @@ class WriteCardViewController: BaseNavigationViewController, View { .observe(on: MainScheduler.asyncInstance) .subscribe(with: self) { object, isProcessing in object.view.endEditing(true) + if isProcessing { object.loadingIndicatorView.startAnimating() } else { @@ -437,14 +444,31 @@ class WriteCardViewController: BaseNavigationViewController, View { } .disposed(by: self.disposeBag) - reactor.state.map(\.isWritten) + reactor.state.map(\.writtenCardId) .filterNil() .distinctUntilChanged() - .filter { $0 } .observe(on: MainScheduler.asyncInstance) - .subscribe(with: self) { object, _ in - object.navigationPop { - NotificationCenter.default.post(name: .reloadData, object: object) + .subscribe(with: self) { object, writtenCardId in + NotificationCenter.default.post(name: .reloadData, object: nil, userInfo: nil) + if reactor.entranceType == .comment { + NotificationCenter.default.post(name: .reloadCommentsData, object: nil, userInfo: nil) + } + + if let navigationController = object.navigationController { + + let detailViewController = DetailViewController() + detailViewController.reactor = reactor.reactorForDetail(with: writtenCardId) + + var viewControllers = navigationController.viewControllers + if (viewControllers.popLast() as? Self) != nil { + + viewControllers.append(detailViewController) + navigationController.setViewControllers(viewControllers, animated: true) + } else { + object.navigationPop() + } + } else { + object.navigationPop() } } .disposed(by: self.disposeBag) diff --git a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift index e535eacc..21c6bff9 100644 --- a/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Write/WriteCardViewReactor.swift @@ -10,11 +10,6 @@ import ReactorKit class WriteCardViewReactor: Reactor { - enum RequestType { - case card - case comment - } - enum Action: Equatable { case landing case updateUserImage(UIImage?, Bool) @@ -27,14 +22,6 @@ class WriteCardViewReactor: Reactor { isStory: Bool, tags: [String] ) - // case writeComment( - // isDistanceShared: Bool, - // content: String, - // font: String, - // imgType: String, - // imgName: String, - // commentTags: [String] - // ) case relatedTags(keyword: String) case updateRelatedTags } @@ -42,7 +29,7 @@ class WriteCardViewReactor: Reactor { enum Mutation { case defaultImages(DefaultImages) case updateUserImage(UIImage?, Bool) - case writeCard(Bool) + case writeCard(String?) case relatedTags([TagInfo]) case updateIsProcessing(Bool) case updateErrors(Int?) @@ -52,9 +39,9 @@ class WriteCardViewReactor: Reactor { fileprivate(set) var shouldUseCoordinates: Bool fileprivate(set) var defaultImages: DefaultImages? fileprivate(set) var userImage: UIImage? - fileprivate(set) var isDownloaded: Bool? - fileprivate(set) var isWritten: Bool? fileprivate(set) var relatedTags: [TagInfo]? + fileprivate(set) var writtenCardId: String? + fileprivate(set) var isDownloaded: Bool? fileprivate(set) var isProcessing: Bool fileprivate(set) var hasErrors: Int? } @@ -63,9 +50,9 @@ class WriteCardViewReactor: Reactor { shouldUseCoordinates: false, defaultImages: nil, userImage: nil, - isDownloaded: nil, - isWritten: nil, relatedTags: nil, + writtenCardId: nil, + isDownloaded: nil, isProcessing: false, hasErrors: nil ) @@ -73,30 +60,24 @@ class WriteCardViewReactor: Reactor { private let dependencies: AppDIContainerable private let cardUseCase: CardUseCase private let tagUseCase: TagUseCase - private let userUseCase: UserUseCase let locationManager: LocationManagerDelegate - let requestType: RequestType + let entranceType: EntranceCardType private let parentCardId: String? - // let parentPungTime: Date? init( dependencies: AppDIContainerable, - type requestType: RequestType, + type entranceType: EntranceCardType = .feed, parentCardId: String? = nil - // parentPungTime: Date? = 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.locationManager = dependencies.rootContainer.resolve(ManagerProviderType.self).locationManager - self.requestType = requestType + self.entranceType = entranceType self.parentCardId = parentCardId - // self.parentCardId = parentCardId - // self.parentPungTime = parentPungTime } func mutate(action: Action) -> Observable { @@ -133,32 +114,6 @@ class WriteCardViewReactor: Reactor { .delay(.milliseconds(1000), scheduler: MainScheduler.instance), .just(.updateIsProcessing(false)) ]) - // case let .writeComment( - // isDistanceShared, - // content, - // font, - // imgType, - // imgName, - // commentTags - // ): - // let coordinate = self.provider.locationManager.coordinate - // let trimedContent = content.trimmingCharacters(in: .whitespacesAndNewlines) - // - // let request: CardRequest = .writeComment( - // id: self.parentCardId ?? "", - // isDistanceShared: !isDistanceShared, - // latitude: coordinate.latitude, - // longitude: coordinate.longitude, - // content: trimedContent, - // font: font, - // imgType: imgType, - // imgName: imgName, - // commentTags: commentTags - // ) - // - // return self.provider.networkManager.request(Status.self, request: request) - // .map { .writeCard($0.httpCode == 201) } - // .catch(self.catchClosure) case let .relatedTags(keyword): return self.tagUseCase.relatedTags(keyword: keyword, size: 8) @@ -177,8 +132,8 @@ class WriteCardViewReactor: Reactor { case let .updateUserImage(userImage, isDownloaded): newState.userImage = userImage newState.isDownloaded = isDownloaded - case let .writeCard(isWritten): - newState.isWritten = isWritten + case let .writeCard(writtenCardId): + newState.writtenCardId = writtenCardId case let .relatedTags(relatedTags): newState.relatedTags = relatedTags case let .updateIsProcessing(isProcessing): @@ -194,13 +149,13 @@ private extension WriteCardViewReactor { func uploadImage(_ image: UIImage) -> Observable { - return self.userUseCase.presignedURL() + return self.cardUseCase.presignedURL() .withUnretained(self) .flatMapLatest { object, presignedInfo -> Observable in if let imageData = image.jpegData(compressionQuality: 0.5), let url = URL(string: presignedInfo.imgUrl) { - return object.userUseCase.uploadImage(imageData, with: url) + return object.cardUseCase.uploadImage(imageData, with: url) .flatMapLatest { isSuccess -> Observable in let imageName = isSuccess ? presignedInfo.imgName : nil @@ -227,7 +182,7 @@ private extension WriteCardViewReactor { if case .default = imageType, let imageName = imageName { - if self.requestType == .card { + if self.entranceType == .feed { return self.cardUseCase.writeCard( isDistanceShared: isDistanceShared, @@ -263,9 +218,9 @@ private extension WriteCardViewReactor { return self.uploadImage(image) .withUnretained(self) .flatMapLatest { object, imageName -> Observable in - guard let imageName = imageName else { return .just(.writeCard(false)) } + guard let imageName = imageName else { return .just(.writeCard(nil)) } - if self.requestType == .card { + if self.entranceType == .feed { return object.cardUseCase.writeCard( isDistanceShared: isDistanceShared, @@ -297,7 +252,7 @@ private extension WriteCardViewReactor { } } - return .just(.writeCard(false)) + return .just(.writeCard(nil)) } var catchClosure: ((Error) throws -> Observable ) { @@ -305,10 +260,18 @@ private extension WriteCardViewReactor { let nsError = error as NSError return .concat([ - .just(.writeCard(false)), + .just(.writeCard(nil)), .just(.updateIsProcessing(false)), .just(.updateErrors(nsError.code)) ]) } } } + + +extension WriteCardViewReactor { + + func reactorForDetail(with targetCardId: String) -> DetailViewReactor { + DetailViewReactor(dependencies: self.dependencies, self.entranceType, type: .navi, with: targetCardId) + } +} diff --git a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift index e76ae1f4..7f7f324b 100644 --- a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift +++ b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift @@ -41,6 +41,8 @@ enum CardRequest: BaseRequest { /// 기본 이미지 조회 case defaultImages + /// 프로필 이미지 업로드할 공간 조회 + case presignedURL /// 글추가 case writeCard( isDistanceShared: Bool, @@ -113,6 +115,9 @@ enum CardRequest: BaseRequest { case .defaultImages: return "/api/images/defaults" + case .presignedURL: + + return "/api/images/card-img" case .writeCard: return "/api/cards" diff --git a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift index 23a5701f..32e197e7 100644 --- a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift +++ b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/UserRequest.swift @@ -17,7 +17,7 @@ enum UserRequest: BaseRequest { case validateNickname(nickname: String) /// 닉네임 업데이트 case updateNickname(nickname: String) - /// 이미지 업로드할 공간 확보 + /// 프로필 이미지 업로드할 공간 조회 case presignedURL /// 이미지 업데이트 case updateImage(imageName: String) diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Outlined/v2_eye_outlined.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Outlined/v2_eye_outlined.imageset/Contents.json new file mode 100644 index 00000000..de4398ae --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Outlined/v2_eye_outlined.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_eye_outlined.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Outlined/v2_eye_outlined.imageset/v2_eye_outlined.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Outlined/v2_eye_outlined.imageset/v2_eye_outlined.svg new file mode 100644 index 00000000..9d2ecb9e --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Outlined/v2_eye_outlined.imageset/v2_eye_outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/SOOUM/SOOUM/Utilities/Typography/UILabel+Typography.swift b/SOOUM/SOOUM/Utilities/Typography/UILabel+Typography.swift index 29a03d83..798f0e9b 100644 --- a/SOOUM/SOOUM/Utilities/Typography/UILabel+Typography.swift +++ b/SOOUM/SOOUM/Utilities/Typography/UILabel+Typography.swift @@ -10,40 +10,16 @@ import UIKit /// https://github.com/Geri-Borbas/iOS.Blog.UILabel_Typography_Extensions extension UILabel { - - fileprivate struct Keys { - static var kUILabelTypography: String = "kUILabelTypography" - static var kUILabelTextObserver: String = "kUILabelTextObserver" - - static func setObjctForTypo(_ typography: Typography) { - withUnsafePointer(to: Self.kUILabelTypography) { - objc_setAssociatedObject(self, $0, typography, .OBJC_ASSOCIATION_RETAIN) - } - } - static func getObjectForTypo() -> Typography? { - withUnsafePointer(to: Self.kUILabelTypography) { - objc_getAssociatedObject(self, $0) as? Typography - } - } - - static func setObjectForObserver(_ textObserver: TextObserver?) { - withUnsafePointer(to: Self.kUILabelTextObserver) { - objc_setAssociatedObject(self, $0, textObserver, .OBJC_ASSOCIATION_RETAIN) - } - } - static func getObjectForObserver() -> TextObserver? { - withUnsafePointer(to: Self.kUILabelTextObserver) { - objc_getAssociatedObject(self, $0) as? TextObserver - } - } - } + + private static var kUILabelTypography: UInt8 = 0 + private static var kUILabelTextObserver: UInt8 = 0 func setTypography( _ typography: Typography, with closure: ((NSMutableAttributedString) -> Void)? = nil ) { - Keys.setObjctForTypo(typography) + objc_setAssociatedObject(self, &Self.kUILabelTypography, typography, .OBJC_ASSOCIATION_RETAIN) self.font = typography.font @@ -81,7 +57,7 @@ extension UILabel { } } get { - return Keys.getObjectForTypo() + return objc_getAssociatedObject(self, &Self.kUILabelTypography) as? Typography } } } @@ -93,11 +69,11 @@ extension UILabel { typealias TextChangeAction = (_ oldValue: String?, _ newValue: String?) -> Void fileprivate var observer: TextObserver? { - get { - Keys.getObjectForObserver() - } set { - Keys.setObjectForObserver(newValue) + objc_setAssociatedObject(self, &Self.kUILabelTextObserver, newValue, .OBJC_ASSOCIATION_RETAIN) + } + get { + return objc_getAssociatedObject(self, &Self.kUILabelTextObserver) as? TextObserver } } diff --git a/SOOUM/SOOUM/Utilities/Typography/UITextField+Typography.swift b/SOOUM/SOOUM/Utilities/Typography/UITextField+Typography.swift index 8df5a65c..38c9798a 100644 --- a/SOOUM/SOOUM/Utilities/Typography/UITextField+Typography.swift +++ b/SOOUM/SOOUM/Utilities/Typography/UITextField+Typography.swift @@ -9,40 +9,15 @@ import UIKit extension UITextField { - fileprivate struct Keys { - static var UITextFieldTypography: String = "UITextFieldTypography" - static var kUITextFieldConstraint: String = "kUITextFieldConstraint" - - static func setObjctForTypo(_ typography: Typography) { - withUnsafePointer(to: Self.UITextFieldTypography) { - objc_setAssociatedObject(self, $0, typography, .OBJC_ASSOCIATION_RETAIN) - } - } - static func getObjectForTypo() -> Typography? { - withUnsafePointer(to: Self.UITextFieldTypography) { - objc_getAssociatedObject(self, $0) as? Typography - } - } - - static func setObjectForConstraint(_ constraint: NSLayoutConstraint?) { - withUnsafePointer(to: Self.kUITextFieldConstraint) { - objc_setAssociatedObject(self, $0, constraint, .OBJC_ASSOCIATION_RETAIN) - } - } - - static func getObjectForConstraint() -> NSLayoutConstraint? { - withUnsafePointer(to: Self.kUITextFieldConstraint) { - objc_getAssociatedObject(self, $0) as? NSLayoutConstraint - } - } - } + private static var KUITextFieldTypography: UInt8 = 0 + private static var kUITextFieldConstraint: UInt8 = 0 func setTypography( _ typography: Typography, with closure: ((inout [NSAttributedString.Key: Any]) -> Void)? = nil ) { - Keys.setObjctForTypo(typography) + objc_setAssociatedObject(self, &Self.KUITextFieldTypography, typography, .OBJC_ASSOCIATION_RETAIN) if let constraint = self.constraint { constraint.constant = typography.lineHeight @@ -70,21 +45,16 @@ extension UITextField { } } get { - return Keys.getObjectForTypo() + return objc_getAssociatedObject(self, &Self.KUITextFieldTypography) as? Typography } } -} - -extension UITextField { - - static var kUITextFieldConstraint: String = "kUITextFieldConstraint" - - fileprivate var constraint: NSLayoutConstraint? { - get { - return Keys.getObjectForConstraint() - } + + private var constraint: NSLayoutConstraint? { set { - Keys.setObjectForConstraint(newValue) + objc_setAssociatedObject(self, &Self.kUITextFieldConstraint, newValue, .OBJC_ASSOCIATION_RETAIN) + } + get { + return objc_getAssociatedObject(self, &Self.kUITextFieldConstraint) as? NSLayoutConstraint } } } diff --git a/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift b/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift index 03a441d9..92f01ef7 100644 --- a/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift +++ b/SOOUM/SOOUM/Utilities/Typography/UITextView+Typography.swift @@ -8,28 +8,15 @@ import UIKit extension UITextView { - - fileprivate struct Keys { - static var kUITextViewTypography: String = "kUITextViewTypography" - - static func setObjctForTypo(_ typography: Typography) { - withUnsafePointer(to: Self.kUITextViewTypography) { - objc_setAssociatedObject(self, $0, typography, .OBJC_ASSOCIATION_RETAIN) - } - } - static func getObjectForTypo() -> Typography? { - withUnsafePointer(to: Self.kUITextViewTypography) { - objc_getAssociatedObject(self, $0) as? Typography - } - } - } + + private static var kUITextViewTypography: UInt8 = 0 func setTypography( _ typography: Typography, with closure: ((inout [NSAttributedString.Key: Any]) -> Void)? = nil ) { - Keys.setObjctForTypo(typography) + objc_setAssociatedObject(self, &Self.kUITextViewTypography, typography, .OBJC_ASSOCIATION_RETAIN) var attributes: [NSAttributedString.Key: Any] = typography.attributes attributes[.font] = typography.font @@ -54,7 +41,7 @@ extension UITextView { } } get { - return Keys.getObjectForTypo() + return objc_getAssociatedObject(self, &Self.kUITextViewTypography) as? Typography } } }