diff --git a/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.pbxproj b/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.pbxproj index 2700944e..07e6d79b 100644 --- a/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.pbxproj +++ b/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.pbxproj @@ -261,9 +261,24 @@ F040E9D32A6D8E8800DF0C4A /* RefreshFilterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F040E9D22A6D8E8800DF0C4A /* RefreshFilterCell.swift */; }; F040E9D52A6D967300DF0C4A /* KeywordFilterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F040E9D42A6D967300DF0C4A /* KeywordFilterCell.swift */; }; F049E8C52A51CAE700E61199 /* ImageAssetKind+StoreTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F049E8C42A51CAE700E61199 /* ImageAssetKind+StoreTag.swift */; }; + F0614E2C2BF9C901008AAF10 /* MyReviewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E282BF9C901008AAF10 /* MyReviewRouter.swift */; }; + F0614E2D2BF9C901008AAF10 /* MyReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E292BF9C901008AAF10 /* MyReviewViewController.swift */; }; + F0614E2E2BF9C901008AAF10 /* MyReviewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E2A2BF9C901008AAF10 /* MyReviewBuilder.swift */; }; + F0614E2F2BF9C901008AAF10 /* MyReviewInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E2B2BF9C901008AAF10 /* MyReviewInteractor.swift */; }; + F0614E312BF9D037008AAF10 /* ProfileHomeViewHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E302BF9D037008AAF10 /* ProfileHomeViewHolder.swift */; }; + F0614E332BF9D2F8008AAF10 /* MyReviewViewHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E322BF9D2F8008AAF10 /* MyReviewViewHolder.swift */; }; + F0614E352BF9D7D5008AAF10 /* ProductInfoStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E342BF9D7D5008AAF10 /* ProductInfoStackView.swift */; }; + F0614E3E2BF9DD01008AAF10 /* MyReviewContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E3D2BF9DD01008AAF10 /* MyReviewContentView.swift */; }; + F0614E412BF9DE43008AAF10 /* MyReviewContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E402BF9DE43008AAF10 /* MyReviewContentConfiguration.swift */; }; + F0614E472BF9E775008AAF10 /* MyDetailReviewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E432BF9E775008AAF10 /* MyDetailReviewRouter.swift */; }; + F0614E482BF9E775008AAF10 /* MyDetailReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E442BF9E775008AAF10 /* MyDetailReviewViewController.swift */; }; + F0614E492BF9E775008AAF10 /* MyDetailReviewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E452BF9E775008AAF10 /* MyDetailReviewBuilder.swift */; }; + F0614E4A2BF9E775008AAF10 /* MyDetailReviewInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E462BF9E775008AAF10 /* MyDetailReviewInteractor.swift */; }; + F0614E4C2BF9EA55008AAF10 /* MyDetailReviewViewHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0614E4B2BF9EA55008AAF10 /* MyDetailReviewViewHolder.swift */; }; F0687BED2A529679004B5EAE /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0687BEC2A529679004B5EAE /* Config.swift */; }; F068A9702A99E76A000AFD52 /* FilterRenderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F068A96F2A99E76A000AFD52 /* FilterRenderable.swift */; }; F069DBE62A30B9850001D3DD /* UIFont+.swift in Sources */ = {isa = PBXBuildFile; fileRef = F069DBE52A30B9850001D3DD /* UIFont+.swift */; }; + F06E3D982C0C428D00D6C195 /* ActionButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06E3D972C0C428D00D6C195 /* ActionButtonCell.swift */; }; F06F40292B35E2FC00897396 /* AdMobManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06F40282B35E2FC00897396 /* AdMobManager.swift */; }; F06F40322B35EE2800897396 /* CurationAdCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06F40312B35EE2800897396 /* CurationAdCell.swift */; }; F06F40372B37361500897396 /* NativeAdView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06F40362B37361500897396 /* NativeAdView.swift */; }; @@ -584,12 +599,27 @@ F040E9D22A6D8E8800DF0C4A /* RefreshFilterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshFilterCell.swift; sourceTree = ""; }; F040E9D42A6D967300DF0C4A /* KeywordFilterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordFilterCell.swift; sourceTree = ""; }; F049E8C42A51CAE700E61199 /* ImageAssetKind+StoreTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageAssetKind+StoreTag.swift"; sourceTree = ""; }; + F0614E282BF9C901008AAF10 /* MyReviewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewRouter.swift; sourceTree = ""; }; + F0614E292BF9C901008AAF10 /* MyReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewViewController.swift; sourceTree = ""; }; + F0614E2A2BF9C901008AAF10 /* MyReviewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewBuilder.swift; sourceTree = ""; }; + F0614E2B2BF9C901008AAF10 /* MyReviewInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewInteractor.swift; sourceTree = ""; }; + F0614E302BF9D037008AAF10 /* ProfileHomeViewHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHomeViewHolder.swift; sourceTree = ""; }; + F0614E322BF9D2F8008AAF10 /* MyReviewViewHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewViewHolder.swift; sourceTree = ""; }; + F0614E342BF9D7D5008AAF10 /* ProductInfoStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoStackView.swift; sourceTree = ""; }; + F0614E3D2BF9DD01008AAF10 /* MyReviewContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewContentView.swift; sourceTree = ""; }; + F0614E402BF9DE43008AAF10 /* MyReviewContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyReviewContentConfiguration.swift; sourceTree = ""; }; + F0614E432BF9E775008AAF10 /* MyDetailReviewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyDetailReviewRouter.swift; sourceTree = ""; }; + F0614E442BF9E775008AAF10 /* MyDetailReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyDetailReviewViewController.swift; sourceTree = ""; }; + F0614E452BF9E775008AAF10 /* MyDetailReviewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyDetailReviewBuilder.swift; sourceTree = ""; }; + F0614E462BF9E775008AAF10 /* MyDetailReviewInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyDetailReviewInteractor.swift; sourceTree = ""; }; + F0614E4B2BF9EA55008AAF10 /* MyDetailReviewViewHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyDetailReviewViewHolder.swift; sourceTree = ""; }; F0687BEC2A529679004B5EAE /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; F068A96F2A99E76A000AFD52 /* FilterRenderable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterRenderable.swift; sourceTree = ""; }; F069DBE52A30B9850001D3DD /* UIFont+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+.swift"; sourceTree = ""; }; F069DBE82A30BA8C0001D3DD /* Pretendard-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Medium.otf"; sourceTree = ""; }; F069DBE92A30BA8C0001D3DD /* Pretendard-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Regular.otf"; sourceTree = ""; }; F069DBEA2A30BA8C0001D3DD /* Pretendard-SemiBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-SemiBold.otf"; sourceTree = ""; }; + F06E3D972C0C428D00D6C195 /* ActionButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonCell.swift; sourceTree = ""; }; F06F40282B35E2FC00897396 /* AdMobManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdMobManager.swift; sourceTree = ""; }; F06F40312B35EE2800897396 /* CurationAdCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurationAdCell.swift; sourceTree = ""; }; F06F40362B37361500897396 /* NativeAdView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAdView.swift; sourceTree = ""; }; @@ -700,6 +730,7 @@ B2EFFAB82ACBF63D007AF2EE /* StarView.swift */, B2EFFABA2ACBF680007AF2EE /* StarRatingView.swift */, B2EFFABC2ACBF69C007AF2EE /* StarRatedView.swift */, + F0614E342BF9D7D5008AAF10 /* ProductInfoStackView.swift */, ); path = Review; sourceTree = ""; @@ -884,6 +915,7 @@ B28205012A346FDD00F9242F /* ProfileHomeInteractor.swift */, F0EE10A22A4B242C00B8DF4F /* ProfileCell.swift */, F00609512A5EED4600A2A79D /* SettingInfo.swift */, + F0614E302BF9D037008AAF10 /* ProfileHomeViewHolder.swift */, ); path = ProfileHome; sourceTree = ""; @@ -1334,6 +1366,8 @@ BA87E3DA2A4623D7000A9DEC /* Entity */, BA87E3C72A45C579000A9DEC /* ProductDetail */, B2538B8F2A3454FA00B7C3F0 /* ProfileHome */, + F0614E272BF9C8D4008AAF10 /* MyReview */, + F0614E422BF9E74D008AAF10 /* MyDetailReview */, F075D0C32B052ED500DD0F5E /* ProfileEdit */, B2538B8E2A3454F100B7C3F0 /* EventHome */, B2538B8D2A3454EA00B7C3F0 /* ProductHome */, @@ -1392,7 +1426,6 @@ BAE9843A2AACFB3400ED22CB /* ImageCell */, BAE9843B2AACFB5F00ED22CB /* InformationCell */, BAE9843C2AACFB7000ED22CB /* ReviewWriteCell */, - BAE9843D2AACFB8C00ED22CB /* ReviewCell */, ); path = Subview; sourceTree = ""; @@ -1517,6 +1550,40 @@ path = EventDetail; sourceTree = ""; }; + F0614E272BF9C8D4008AAF10 /* MyReview */ = { + isa = PBXGroup; + children = ( + F0614E3F2BF9DE2D008AAF10 /* View */, + F0614E282BF9C901008AAF10 /* MyReviewRouter.swift */, + F0614E292BF9C901008AAF10 /* MyReviewViewController.swift */, + F0614E2A2BF9C901008AAF10 /* MyReviewBuilder.swift */, + F0614E2B2BF9C901008AAF10 /* MyReviewInteractor.swift */, + F0614E322BF9D2F8008AAF10 /* MyReviewViewHolder.swift */, + ); + path = MyReview; + sourceTree = ""; + }; + F0614E3F2BF9DE2D008AAF10 /* View */ = { + isa = PBXGroup; + children = ( + F0614E3D2BF9DD01008AAF10 /* MyReviewContentView.swift */, + F0614E402BF9DE43008AAF10 /* MyReviewContentConfiguration.swift */, + ); + path = View; + sourceTree = ""; + }; + F0614E422BF9E74D008AAF10 /* MyDetailReview */ = { + isa = PBXGroup; + children = ( + F0614E432BF9E775008AAF10 /* MyDetailReviewRouter.swift */, + F0614E442BF9E775008AAF10 /* MyDetailReviewViewController.swift */, + F0614E452BF9E775008AAF10 /* MyDetailReviewBuilder.swift */, + F0614E462BF9E775008AAF10 /* MyDetailReviewInteractor.swift */, + F0614E4B2BF9EA55008AAF10 /* MyDetailReviewViewHolder.swift */, + ); + path = MyDetailReview; + sourceTree = ""; + }; F0687BEB2A529666004B5EAE /* Config */ = { isa = PBXGroup; children = ( @@ -1543,6 +1610,23 @@ path = Font; sourceTree = ""; }; + F06E3D952C0C401B00D6C195 /* EmptyCell */ = { + isa = PBXGroup; + children = ( + BAE442C52A6D7B9E00BD6582 /* EmptyCell.swift */, + BAE442C72A6D7C3900BD6582 /* EmptyCell+ViewHolder.swift */, + ); + path = EmptyCell; + sourceTree = ""; + }; + F06E3D962C0C412500D6C195 /* ActionButtonCell */ = { + isa = PBXGroup; + children = ( + F06E3D972C0C428D00D6C195 /* ActionButtonCell.swift */, + ); + path = ActionButtonCell; + sourceTree = ""; + }; F06F40272B35E2D800897396 /* Manager */ = { isa = PBXGroup; children = ( @@ -1706,10 +1790,11 @@ F0D6FFA72A39F57600C55E27 /* Cell */ = { isa = PBXGroup; children = ( + F06E3D962C0C412500D6C195 /* ActionButtonCell */, + BAE9843D2AACFB8C00ED22CB /* ReviewCell */, + F06E3D952C0C401B00D6C195 /* EmptyCell */, F0B6E6C82AB8A2E700AA7006 /* ProductCell */, B24F1D302A431E1700AA03DC /* ConvenienceStoreCell.swift */, - BAE442C52A6D7B9E00BD6582 /* EmptyCell.swift */, - BAE442C72A6D7C3900BD6582 /* EmptyCell+ViewHolder.swift */, F0C698642A6BA9B00019C677 /* CategoryFilterCell.swift */, F040E9D22A6D8E8800DF0C4A /* RefreshFilterCell.swift */, F040E9D42A6D967300DF0C4A /* KeywordFilterCell.swift */, @@ -1911,6 +1996,7 @@ F0C698732A6D4DBF0019C677 /* TopCommonSectionLayout.swift in Sources */, F097EF2E2A57187400A7FB9C /* ProfileUrl.swift in Sources */, B21C1A8C2A513FBB007D98E9 /* RIBs+.swift in Sources */, + F0614E4C2BF9EA55008AAF10 /* MyDetailReviewViewHolder.swift in Sources */, BA1807622A2F21D900295398 /* URLRequest+.swift in Sources */, BA18C0B12A759EAC007D00BD /* ProductSearchSortBottomSheetViewController.swift in Sources */, BA18C0922A753464007D00BD /* SearchFilterHeader+ViewHolder.swift in Sources */, @@ -1933,6 +2019,7 @@ F0168D542A500AC900978ED9 /* EventDetailInteractor.swift in Sources */, BA87501C2A519A9200B80088 /* ImageAssetKind+StoreIcon.swift in Sources */, B24891632B3AD2E5009D0FBF /* HomeBannerEntity.swift in Sources */, + F0614E312BF9D037008AAF10 /* ProfileHomeViewHolder.swift in Sources */, B2F169D62A347208008199D8 /* RootTabBarBuilder.swift in Sources */, BA50FD092A680D2500721782 /* SearchBarView.swift in Sources */, BAE984472AACFFEF00ED22CB /* ProductDetailReviewCell.swift in Sources */, @@ -1973,6 +2060,10 @@ BA7532E22A595B140002659A /* UserAuthService.swift in Sources */, F0E915D52AAC9E3F000919DA /* CommonProductPageViewControllerRenderable.swift in Sources */, BA8924562A582EE700D1B097 /* NewTagBigView.swift in Sources */, + F0614E2C2BF9C901008AAF10 /* MyReviewRouter.swift in Sources */, + F0614E2D2BF9C901008AAF10 /* MyReviewViewController.swift in Sources */, + F0614E2E2BF9C901008AAF10 /* MyReviewBuilder.swift in Sources */, + F0614E2F2BF9C901008AAF10 /* MyReviewInteractor.swift in Sources */, F075D0C82B052EF300DD0F5E /* ProfileEditRouter.swift in Sources */, BA6CAB802AE84C6600B9E1BF /* UserInfoService.swift in Sources */, BAE442C22A6D2C7A00BD6582 /* LoadingReusableView.swift in Sources */, @@ -1989,6 +2080,7 @@ B24891672B3C2525009D0FBF /* EventBannerHeaderView.swift in Sources */, BA18C0A62A7596F5007D00BD /* BottomToTopPresentAnimator.swift in Sources */, BA18075C2A2F189A00295398 /* UIButton+.swift in Sources */, + F0614E352BF9D7D5008AAF10 /* ProductInfoStackView.swift in Sources */, BAD6AEFD2AEC52550062B16F /* ReviewEvaluationLabelView.swift in Sources */, BA50FD042A680AA900721782 /* ProductSearchInteractor.swift in Sources */, B28A592A2A6FCFED00431F39 /* SortFilterCell.swift in Sources */, @@ -2023,6 +2115,7 @@ BAE442C82A6D7C3900BD6582 /* EmptyCell+ViewHolder.swift in Sources */, B24F1D4B2A4BB82D00AA03DC /* TitleNavigationView.swift in Sources */, F00022462A5280FC00FFB4A4 /* NetworkRequestBuilder.swift in Sources */, + F06E3D982C0C428D00D6C195 /* ActionButtonCell.swift in Sources */, F00352242A62BD9F00A66FF9 /* UserLoginStatusEntity.swift in Sources */, B28A59272A6FCE9700431F39 /* ProductFilterSectionLayout.swift in Sources */, B242590E2A5947E7006C5223 /* PageableEntity.swift in Sources */, @@ -2052,6 +2145,7 @@ BA6CAB822AE8509000B9E1BF /* ReviewLikeCountEntity.swift in Sources */, F0D6FFAB2A39F89D00C55E27 /* UIView+.swift in Sources */, F0C2B9F12AA4740000ACF3D7 /* EventHomeViewHolder.swift in Sources */, + F0614E3E2BF9DD01008AAF10 /* MyReviewContentView.swift in Sources */, BAE984412AACFC3600ED22CB /* ProductDetailImageCell+ViewHolder.swift in Sources */, B24259102A594862006C5223 /* SortEntity.swift in Sources */, B24CAAE32A55AC2D005BE499 /* NotificationListBuilder.swift in Sources */, @@ -2066,6 +2160,10 @@ BA1807552A2F151F00295398 /* ImageAssetKind.swift in Sources */, F06F40372B37361500897396 /* NativeAdView.swift in Sources */, B22496522ABC1BE400BC6B09 /* ReviewPopupRouter.swift in Sources */, + F0614E472BF9E775008AAF10 /* MyDetailReviewRouter.swift in Sources */, + F0614E482BF9E775008AAF10 /* MyDetailReviewViewController.swift in Sources */, + F0614E492BF9E775008AAF10 /* MyDetailReviewBuilder.swift in Sources */, + F0614E4A2BF9E775008AAF10 /* MyDetailReviewInteractor.swift in Sources */, F0168D512A500AC900978ED9 /* EventDetailRouter.swift in Sources */, BAF8725D2AD404C0001427DF /* ProductGiftEntity.swift in Sources */, BA87E3DC2A4623E3000A9DEC /* GiftItemEntity.swift in Sources */, @@ -2129,6 +2227,7 @@ BA87E3D72A45D40A000A9DEC /* GiftInformationView.swift in Sources */, F0B90DCC2ADD2167005DE34A /* FavoriteHomePageViewController.swift in Sources */, BA83225E2AB77E9E00D2E1EB /* ReviewEvaluationState.swift in Sources */, + F0614E332BF9D2F8008AAF10 /* MyReviewViewHolder.swift in Sources */, F0E915B12AA87BB4000919DA /* ControlSubscription.swift in Sources */, B28205042A346FDD00F9242F /* ProfileHomeBuilder.swift in Sources */, BAE984432AACFE3200ED22CB /* ProductDetailInformationCell.swift in Sources */, @@ -2198,6 +2297,7 @@ B224965B2ABD8A7700BC6B09 /* UIImage+.swift in Sources */, B28A59242A6FB2B200431F39 /* ProductFilterBuilder.swift in Sources */, B2C25CDB2B65166E005C6F0D /* ProductType.swift in Sources */, + F0614E412BF9DE43008AAF10 /* MyReviewContentConfiguration.swift in Sources */, F0DEABA02AAF410B00941A5F /* FavoriteHomeBuilder.swift in Sources */, BA00B7392A46B8BF00BB3795 /* Spacing.swift in Sources */, BA00B73C2A46B98700BB3795 /* SnapKit+.swift in Sources */, @@ -2490,7 +2590,7 @@ repositoryURL = "https://github.com/googleads/swift-package-manager-google-mobile-ads.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 10.14.0; + minimumVersion = 11.2.0; }; }; F0C9E0C62A7BF63600A53166 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fbd11f36..728b9e7a 100644 --- a/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Pyonsnal-Color/Pyonsnal-Color.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "2ae251365e2bd450b633726d7085f35a9a46a0f82ec34d41708921f1b80bb996", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -41,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "98a00258d4518b7521253a70b7f70bb76d2120fe", - "version" : "9.2.4" + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" } }, { @@ -50,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "58d03d22beae762eaddbd30cb5a61af90d4b309f", - "version" : "7.11.3" + "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6", + "version" : "7.13.3" } }, { @@ -149,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", "state" : { - "revision" : "70516c9e799a366ff90c1a70c09c48f7c076fd8a", - "version" : "10.14.0" + "revision" : "746253bf8803826e4de14c9e619739733ea8fb3b", + "version" : "11.5.0" } }, { @@ -158,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git", "state" : { - "revision" : "129fa838520cd02174f890ae0cfe0242e60714ae", - "version" : "2.1.0" + "revision" : "9b68aa69fb508f0274853e226c734151a973c7b7", + "version" : "2.4.0" } }, { @@ -172,5 +173,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ActionButtonCell/ActionButtonCell.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ActionButtonCell/ActionButtonCell.swift new file mode 100644 index 00000000..bd529623 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ActionButtonCell/ActionButtonCell.swift @@ -0,0 +1,67 @@ +// +// ActionButtonCell.swift +// Pyonsnal-Color +// +// Created by 조소정 on 6/2/24. +// + +import UIKit +import Combine + +protocol ActionButtonCellDelegate: AnyObject { + func actionButtonDidTap() +} + +final class ActionButtonCell: UICollectionViewCell { + // MARK: - Interfaces + enum Constants { + enum Size { + static let actionButtonHeight: CGFloat = 40 + static let actionButtonRadius: CGFloat = 16 + } + + enum Text { + static let writeReview = "상품 리뷰 작성 하기" + static let showProduct = "상품 보러 가기" + } + } + + weak var delegate: ActionButtonCellDelegate? + private var cancellables = Set() + + // MARK: - Initializer + override init(frame: CGRect) { + super.init(frame: .zero) + self.setConstraints() + self.bindActions() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + let actionButton: UIButton = { + let button = UIButton(frame: .zero) + button.makeRounded(with: Constants.Size.actionButtonRadius) + button.backgroundColor = .black + button.titleLabel?.textColor = .white + button.titleLabel?.font = .body2m + return button + }() + + // MARK: - Private Methods + private func setConstraints() { + self.addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.height.equalTo(Constants.Size.actionButtonHeight) + make.top.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(.spacing16) + } + } + + private func bindActions() { + actionButton.tapPublisher.sink { [weak self] in + self?.delegate?.actionButtonDidTap() + }.store(in: &cancellables) + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell+ViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell/EmptyCell+ViewHolder.swift similarity index 100% rename from Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell+ViewHolder.swift rename to Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell/EmptyCell+ViewHolder.swift diff --git a/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell/EmptyCell.swift similarity index 100% rename from Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell.swift rename to Pyonsnal-Color/Pyonsnal-Color/Common/Cell/EmptyCell/EmptyCell.swift diff --git a/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ProductCell/ProductCell.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ProductCell/ProductCell.swift index 65a1bd9c..4a7524bc 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ProductCell/ProductCell.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ProductCell/ProductCell.swift @@ -52,7 +52,7 @@ final class ProductCell: UICollectionViewCell { }.store(in: &cancellable) } - func updateCell(with product: (ProductDetailEntity)?) { + func updateCell(with product: ProductDetailEntity?) { guard let product else { return } self.product = product viewHolder.titleLabel.text = product.name diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewCell/ProductDetailReviewCell+ViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ReviewCell/ProductDetailReviewCell+ViewHolder.swift similarity index 97% rename from Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewCell/ProductDetailReviewCell+ViewHolder.swift rename to Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ReviewCell/ProductDetailReviewCell+ViewHolder.swift index 92758b8c..cbd2714d 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewCell/ProductDetailReviewCell+ViewHolder.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ReviewCell/ProductDetailReviewCell+ViewHolder.swift @@ -111,6 +111,10 @@ extension ProductDetailReviewCell { make.leading.trailing.equalToSuperview().inset(.spacing16) } + reviewTagListView.snp.makeConstraints { make in + make.height.equalTo(28) + } + reviewLabel.snp.makeConstraints { make in make.top.equalTo(contentStackView.snp.bottom).offset(.spacing12) make.leading.trailing.equalToSuperview().inset(.spacing20) diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewCell/ProductDetailReviewCell.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ReviewCell/ProductDetailReviewCell.swift similarity index 91% rename from Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewCell/ProductDetailReviewCell.swift rename to Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ReviewCell/ProductDetailReviewCell.swift index d012fcd1..71eab0ea 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewCell/ProductDetailReviewCell.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/Common/Cell/ReviewCell/ProductDetailReviewCell.swift @@ -16,6 +16,7 @@ final class ProductDetailReviewCell: UICollectionViewCell { // MARK: - Declaration struct Payload { + var hasEvaluateView: Bool = true let review: ReviewEntity } @@ -92,7 +93,6 @@ final class ProductDetailReviewCell: UICollectionViewCell { } else { viewHolder.reviewLabel.isHidden = false - viewHolder.reviewLabel.snp.makeConstraints { make in make.top.equalTo(viewHolder.contentStackView.snp.bottom).offset(.spacing12) make.leading.trailing.equalToSuperview().inset(.spacing20) @@ -114,6 +114,17 @@ final class ProductDetailReviewCell: UICollectionViewCell { isSelected: review.hateCount.writerIds.contains(UserInfoService.shared.memberID ?? 0), count: review.hateCount.hateCount ) + + if !payload.hasEvaluateView { + viewHolder.goodButton.isHidden = true + viewHolder.badButton.isHidden = true + viewHolder.goodButton.snp.remakeConstraints { + $0.height.equalTo(0) + } + viewHolder.badButton.snp.remakeConstraints { + $0.height.equalTo(0) + } + } } @objc private func goodButtonAction(_ sender: UITapGestureRecognizer) { diff --git a/Pyonsnal-Color/Pyonsnal-Color/Common/Review/ProductInfoStackView.swift b/Pyonsnal-Color/Pyonsnal-Color/Common/Review/ProductInfoStackView.swift new file mode 100644 index 00000000..768dabf8 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/Common/Review/ProductInfoStackView.swift @@ -0,0 +1,121 @@ +// +// ProductInfoStackView.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import UIKit +import SnapKit + +class ProductInfoStackView: UIStackView { + enum Mode { + case starRating // 별점 + case date // 날짜 + case tastes // 취향 태그 + } + + let productImageView: UIImageView = { + let imageView = UIImageView() + imageView.makeBorder(width: 1, color: UIColor.gray200.cgColor) + imageView.makeRounded(with: .spacing16) + return imageView + }() + + private let productInformationStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = .spacing8 + stackView.alignment = .leading + stackView.distribution = .equalSpacing + return stackView + }() + + let storeImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + let productNameLabel: UILabel = { + let label = UILabel() + label.font = .body3m + label.numberOfLines = 2 + return label + }() + + lazy var starRatedView = StarRatedView(score: 0) + + let dateLabel: UILabel = { + let label = UILabel() + label.font = .body3r + label.textColor = .gray400 + return label + }() + + let tastesLabel: UILabel = { + let label = UILabel() + label.font = .body3r + label.textColor = .red500 + label.numberOfLines = 2 + return label + }() + + init(mode: Mode) { + super.init(frame: .zero) + self.initialize() + self.configureView() + self.addArrangedSubview(with: mode) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setTastes(tastesTag: [String]) { + let tastes = tastesTag.map { "# \($0)"}.joined(separator: " ") + self.tastesLabel.text = tastes + } + + private func initialize() { + self.axis = .horizontal + self.alignment = .center + self.spacing = .spacing16 + self.isLayoutMarginsRelativeArrangement = true + self.layoutMargins = .init( + top: .spacing24, + left: .spacing16, + bottom: .spacing24, + right: .spacing16 + ) + } + + private func configureView() { + self.addArrangedSubview(productImageView) + self.addArrangedSubview(productInformationStackView) + + self.productInformationStackView.addArrangedSubview(storeImageView) + self.productInformationStackView.addArrangedSubview(productNameLabel) + self.productNameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + self.tastesLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + + self.productImageView.snp.makeConstraints { + $0.width.height.equalTo(100) + } + + self.storeImageView.snp.makeConstraints { + $0.height.equalTo(20) + } + } + + private func addArrangedSubview(with mode: Mode) { + switch mode { + case .starRating: + self.productInformationStackView.addArrangedSubview(starRatedView) + case .date: + self.productInformationStackView.addArrangedSubview(dateLabel) + case .tastes: + self.productInformationStackView.addArrangedSubview(tastesLabel) + } + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewController.swift b/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewController.swift index 5439de6d..969d78a5 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewController.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewController.swift @@ -69,8 +69,8 @@ final class DetailReviewViewController: UIViewController, private func configureView() { view.backgroundColor = .white viewHolder.detailReviewTextView.text = Constant.textViewPlaceholder - viewHolder.productImageView.setImage(with: productDetail.imageURL) - viewHolder.productNameLabel.text = productDetail.name + viewHolder.productInfoStackView.productImageView.setImage(with: productDetail.imageURL) + viewHolder.productInfoStackView.productNameLabel.text = productDetail.name setResizedStoreIcon(productDetail.storeType.storeIcon.image) updateStarRatedView(score: score) viewHolder.totalScrollView.addTapGesture( @@ -167,16 +167,16 @@ final class DetailReviewViewController: UIViewController, } private func updateStarRatedView(score: Int) { - viewHolder.starRatedView.updateScore(to: Double(score)) + viewHolder.productInfoStackView.starRatedView.updateScore(to: Double(score)) } private func setResizedStoreIcon(_ image: UIImage?) { - viewHolder.storeImageView.image = image + viewHolder.productInfoStackView.storeImageView.image = image if let storeIcon = image { let ratio = storeIcon.size.width / storeIcon.size.height let newWidth = Constant.storeIconHeight * ratio - viewHolder.storeImageView.snp.makeConstraints { + viewHolder.productInfoStackView.storeImageView.snp.makeConstraints { $0.width.equalTo(newWidth) } } diff --git a/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewHolder.swift index c148f4c3..c92ddf4f 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewHolder.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/DetailReview/DetailReviewViewHolder.swift @@ -48,52 +48,11 @@ extension DetailReviewViewController { return stackView }() - private let productTotalStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .center - stackView.spacing = .spacing16 - stackView.isLayoutMarginsRelativeArrangement = true - stackView.layoutMargins = .init( - top: .spacing24, - left: .spacing16, - bottom: .spacing24, - right: 0 - ) - return stackView - }() - - let productImageView: UIImageView = { - let imageView = UIImageView() - imageView.makeBorder(width: 1, color: UIColor.gray200.cgColor) - imageView.makeRounded(with: .spacing16) - return imageView - }() - - private let productInformationStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = .spacing8 - stackView.alignment = .leading - stackView.distribution = .equalSpacing - return stackView - }() - - let storeImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - return imageView - }() - - let productNameLabel: UILabel = { - let label = UILabel() - label.font = .body3m - label.numberOfLines = 1 - return label + let productInfoStackView: ProductInfoStackView = { + let infoStackView = ProductInfoStackView(mode: .starRating) + return infoStackView }() - let starRatedView = StarRatedView(score: 0) - private let separatorView: UIView = { let view = UIView() view.backgroundColor = .gray100 @@ -225,19 +184,12 @@ extension DetailReviewViewController { applyButtonBackgroundView.addSubview(applyReviewButton) - contentStackView.addArrangedSubview(productTotalStackView) + contentStackView.addArrangedSubview(productInfoStackView) contentStackView.addArrangedSubview(separatorView) contentStackView.addArrangedSubview(reviewButtonStackView) contentStackView.addArrangedSubview(detailReviewStackView) contentStackView.addArrangedSubview(imageUploadStackView) - productTotalStackView.addArrangedSubview(productImageView) - productTotalStackView.addArrangedSubview(productInformationStackView) - - productInformationStackView.addArrangedSubview(storeImageView) - productInformationStackView.addArrangedSubview(productNameLabel) - productInformationStackView.addArrangedSubview(starRatedView) - reviewButtonStackView.addArrangedSubview(tasteReview) reviewButtonStackView.addArrangedSubview(qualityReview) reviewButtonStackView.addArrangedSubview(priceReview) @@ -281,21 +233,13 @@ extension DetailReviewViewController { $0.height.equalToSuperview() } - productTotalStackView.snp.makeConstraints { + productInfoStackView.snp.makeConstraints { $0.leading.top.trailing.equalToSuperview() } - productImageView.snp.makeConstraints { - $0.width.height.equalTo(100) - } - - storeImageView.snp.makeConstraints { - $0.height.equalTo(20) - } - separatorView.snp.makeConstraints { $0.leading.trailing.equalToSuperview() - $0.top.equalTo(productTotalStackView.snp.bottom) + $0.top.equalTo(productInfoStackView.snp.bottom) $0.height.equalTo(12) } diff --git a/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailEntity.swift b/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailEntity.swift index 90254a17..64913bce 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailEntity.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailEntity.swift @@ -9,12 +9,12 @@ import Foundation struct ProductDetailEntity { let id: String - let storeType: ConvenienceStore // + let storeType: ConvenienceStore let imageURL: URL let name: String let price: String - let eventType: EventTag? // - let productType: ProductType // + let eventType: EventTag? + let productType: ProductType let updatedTime: String let description: String? let isNew: Bool? diff --git a/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailSectionItem.swift b/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailSectionItem.swift index 2eb16e8e..e78f7244 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailSectionItem.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/Entity/Products/ProductDetailSectionItem.swift @@ -11,5 +11,6 @@ enum ProductDetailSectionItem { case image(imageURL: URL) case information(product: ProductDetailEntity) case reviewWrite(score: Double, reviewsCount: Int, sortItem: FilterItemEntity) + case actionButton case review(productReview: ReviewEntity) } diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewBuilder.swift b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewBuilder.swift new file mode 100644 index 00000000..ecafa742 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewBuilder.swift @@ -0,0 +1,53 @@ +// +// MyDetailReviewBuilder.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs + +protocol MyDetailReviewDependency: Dependency { + // TODO: Declare the set of dependencies required by this RIB, but cannot be + // created by this RIB. +} + +final class MyDetailReviewComponent: Component { + var productDetail: ProductDetailEntity + var review: ReviewEntity + + init(dependency: MyDetailReviewDependency, productDetail: ProductDetailEntity, review: ReviewEntity) { + self.productDetail = productDetail + self.review = review + super.init(dependency: dependency) + } +} + +// MARK: - Builder + +protocol MyDetailReviewBuildable: Buildable { + func build( + withListener listener: MyDetailReviewListener, + productDetail: ProductDetailEntity, + review: ReviewEntity + ) -> MyDetailReviewRouting +} + +final class MyDetailReviewBuilder: Builder, MyDetailReviewBuildable { + + override init(dependency: MyDetailReviewDependency) { + super.init(dependency: dependency) + } + + func build( + withListener listener: MyDetailReviewListener, + productDetail: ProductDetailEntity, + review: ReviewEntity + ) -> MyDetailReviewRouting { + let component = MyDetailReviewComponent(dependency: dependency, productDetail: productDetail, review: review) + let viewController = MyDetailReviewViewController() + let interactor = MyDetailReviewInteractor(presenter: viewController, component: component) + interactor.listener = listener + return MyDetailReviewRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewInteractor.swift b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewInteractor.swift new file mode 100644 index 00000000..200c943e --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewInteractor.swift @@ -0,0 +1,50 @@ +// +// MyDetailReviewInteractor.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs + +protocol MyDetailReviewRouting: ViewableRouting { + // TODO: Declare methods the interactor can invoke to manage sub-tree via the router. +} + +protocol MyDetailReviewPresentable: Presentable { + var listener: MyDetailReviewPresentableListener? { get set } + func update(with productDetail: ProductDetailEntity, review: ReviewEntity) +} + +protocol MyDetailReviewListener: AnyObject { + func didTapBackButton() +} + +final class MyDetailReviewInteractor: PresentableInteractor, MyDetailReviewInteractable, MyDetailReviewPresentableListener { + + weak var router: MyDetailReviewRouting? + weak var listener: MyDetailReviewListener? + private let component: MyDetailReviewComponent + + init( + presenter: MyDetailReviewPresentable, + component: MyDetailReviewComponent + ) { + self.component = component + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + self.presenter.update(with: component.productDetail, review: component.review) + } + + override func willResignActive() { + super.willResignActive() + } + + func didTapBackButton() { + listener?.didTapBackButton() + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewRouter.swift b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewRouter.swift new file mode 100644 index 00000000..8d9c8865 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewRouter.swift @@ -0,0 +1,26 @@ +// +// MyDetailReviewRouter.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs + +protocol MyDetailReviewInteractable: Interactable { + var router: MyDetailReviewRouting? { get set } + var listener: MyDetailReviewListener? { get set } +} + +protocol MyDetailReviewViewControllable: ViewControllable { + // TODO: Declare methods the router invokes to manipulate the view hierarchy. +} + +final class MyDetailReviewRouter: ViewableRouter, MyDetailReviewRouting { + + // TODO: Constructor inject child builder protocols to allow building children. + override init(interactor: MyDetailReviewInteractable, viewController: MyDetailReviewViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewViewController.swift b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewViewController.swift new file mode 100644 index 00000000..776077e2 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewViewController.swift @@ -0,0 +1,150 @@ +// +// MyDetailReviewViewController.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs +import UIKit + +protocol MyDetailReviewPresentableListener: AnyObject { + func didTapBackButton() +} + +final class MyDetailReviewViewController: UIViewController, + MyDetailReviewPresentable, + MyDetailReviewViewControllable { + // MARK: Interfaces + private typealias DataSource = UICollectionViewDiffableDataSource + private typealias Snapshot = NSDiffableDataSourceSnapshot + + private enum Section { + case main + } + + private enum Item: Hashable { + case productDetail(product: ProductDetailEntity) + case review(review: ReviewEntity) + case landing + } + + weak var listener: MyDetailReviewPresentableListener? + + // MARK: Private Properties + private let viewHolder: ViewHolder = .init() + private var dataSource: DataSource? + private var productDetail: ProductDetailEntity? + private var review: ReviewEntity? + + // MARK: View Life Cycles + override func viewDidLoad() { + super.viewDidLoad() + viewHolder.place(in: view) + viewHolder.configureConstraints(for: view) + self.configureUI() + self.bindActions() + self.configureCollectionView() + self.configureDataSource() + self.applySnapshot(with: productDetail, review: review) + } + + func update(with productDetail: ProductDetailEntity, review: ReviewEntity) { + self.productDetail = productDetail + self.review = review + } + + // MARK: Private Methods + private func configureUI() { + self.view.backgroundColor = .white + } + + private func bindActions() { + self.viewHolder.backNavigationView.delegate = self + } + + private func configureCollectionView() { + self.registerCollectionViewCells() + self.viewHolder.collectionView.delegate = self + } + + private func registerCollectionViewCells() { + self.viewHolder.collectionView.register(ProductDetailReviewCell.self) + self.viewHolder.collectionView.register( + UICollectionViewCell.self, + forCellWithReuseIdentifier: MyReviewContentView.identifier + ) + self.viewHolder.collectionView.register(ActionButtonCell.self) + } + + private func configureDataSource() { + dataSource = DataSource(collectionView: self.viewHolder.collectionView) { collectionView, indexPath, itemIdentifier in + switch itemIdentifier { + case .productDetail(let product): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyReviewContentView.identifier, for: indexPath) + cell.contentConfiguration = MyReviewContentConfiguration( + mode: .tastes, + storeImageIcon: product.storeType, + imageUrl: product.imageURL, + title: "프링글스) 매콤한맛(대) 프링글스) 매콤한맛(대)프링글스) 매콤한맛(대)프링글스) 매콤한맛", + tastesTag: ["카페인 러버", "헬창", "캐릭터컬렉터", "캐릭터컬렉터"] + ) + return cell + case .review(let review): + let cell: ProductDetailReviewCell = collectionView.dequeueReusableCell(for: indexPath) + cell.payload = .init(hasEvaluateView: false, review: review) + return cell + case .landing: + let cell: ActionButtonCell = collectionView.dequeueReusableCell(for: indexPath) + cell.actionButton.setText(with: ActionButtonCell.Constants.Text.showProduct) + return cell + } + } + } + + private func applySnapshot(with productDetail: ProductDetailEntity?, review: ReviewEntity?) { + guard let productDetail, let review else { return } + var snapshot = Snapshot() + snapshot.appendSections([.main]) + snapshot.appendItems([.productDetail(product: productDetail)]) + snapshot.appendItems([.review(review: review)]) + snapshot.appendItems([.landing]) + dataSource?.apply(snapshot, animatingDifferences: false) + } +} + +// MARK: - UICollectionViewDelegateFlowLayout +extension MyDetailReviewViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + guard let item = dataSource?.itemIdentifier(for: indexPath) else { return CGSize.zero } + let screenWidth = collectionView.bounds.width + switch item { + case .productDetail: + return .init(width: screenWidth, height: 136) + case .review(let review): + let estimateHeight: CGFloat = 1000 + let cell = ProductDetailReviewCell() + cell.frame = .init( + origin: .zero, + size: .init(width: screenWidth, height: estimateHeight) + ) + cell.payload = .init(hasEvaluateView: false, review: review) + cell.layoutIfNeeded() + let estimateSize = cell.systemLayoutSizeFitting( + .init(width: screenWidth, height: UIView.layoutFittingCompressedSize.height), + withHorizontalFittingPriority: .required, + verticalFittingPriority: .defaultLow + ) + return estimateSize + case .landing: + return .init(width: screenWidth, height: 40) + } + } +} + +// MARK: - BackNavigationViewDelegate +extension MyDetailReviewViewController: BackNavigationViewDelegate { + func didTapBackButton() { + listener?.didTapBackButton() + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewViewHolder.swift new file mode 100644 index 00000000..dbd67b4a --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyDetailReview/MyDetailReviewViewHolder.swift @@ -0,0 +1,53 @@ +// +// MyDetailReviewViewHolder.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import UIKit +import SnapKit + +extension MyDetailReviewViewController { + enum Text { + static let navigationTitleView = "상세 리뷰" + } + + enum Size { + static let productInfoStackView: CGFloat = 146 + static let dividerHeight: CGFloat = 1 + } + + class ViewHolder: ViewHolderable { + let backNavigationView: BackNavigationView = { + let navigationView = BackNavigationView() + navigationView.payload = .init(mode: .text) + navigationView.setText(with: Text.navigationTitleView) + return navigationView + }() + + let collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + return UICollectionView(frame: .zero, collectionViewLayout: layout) + }() + + func place(in view: UIView) { + view.addSubview(backNavigationView) + view.addSubview(collectionView) + } + + func configureConstraints(for view: UIView) { + backNavigationView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.leading.trailing.equalTo(view) + } + + collectionView.snp.makeConstraints { + $0.top.equalTo(backNavigationView.snp.bottom) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) + } + } + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewBuilder.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewBuilder.swift new file mode 100644 index 00000000..a1b119b1 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewBuilder.swift @@ -0,0 +1,35 @@ +// +// MyReviewBuilder.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs + +protocol MyReviewDependency: Dependency { } + +final class MyReviewComponent: Component, + MyDetailReviewDependency {} + +// MARK: - Builder + +protocol MyReviewBuildable: Buildable { + func build(withListener listener: MyReviewListener) -> MyReviewRouting +} + +final class MyReviewBuilder: Builder, MyReviewBuildable { + + override init(dependency: MyReviewDependency) { + super.init(dependency: dependency) + } + + func build(withListener listener: MyReviewListener) -> MyReviewRouting { + let component = MyReviewComponent(dependency: dependency) + let viewController = MyReviewViewController() + let myDetailReviewBuilder = MyDetailReviewBuilder(dependency: component) + let interactor = MyReviewInteractor(presenter: viewController) + interactor.listener = listener + return MyReviewRouter(interactor: interactor, viewController: viewController, myDetailReviewBuilder: myDetailReviewBuilder) + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewInteractor.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewInteractor.swift new file mode 100644 index 00000000..5380c48d --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewInteractor.swift @@ -0,0 +1,59 @@ +// +// MyReviewInteractor.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs + +protocol MyReviewRouting: ViewableRouting { + func attachMyDetailReview(productDetail: ProductDetailEntity, review: ReviewEntity) + func detachMyDetailReview() +} + +protocol MyReviewPresentable: Presentable { + var listener: MyReviewPresentableListener? { get set } + // TODO: Declare methods the interactor can invoke the presenter to present data. +} + +protocol MyReviewListener: AnyObject { + func detachMyReview() +} + +final class MyReviewInteractor: PresentableInteractor, MyReviewInteractable, MyReviewPresentableListener { + + weak var router: MyReviewRouting? + weak var listener: MyReviewListener? + + // TODO: Add additional dependencies to constructor. Do not perform any logic + // in constructor. + override init(presenter: MyReviewPresentable) { + super.init(presenter: presenter) + presenter.listener = self + } + + override func didBecomeActive() { + super.didBecomeActive() + // TODO: Implement business logic here. + } + + override func willResignActive() { + super.willResignActive() + // TODO: Pause any business logic. + } + + func didTapMyDetailReview(with productDetail: ProductDetailEntity, reviewId: String) { + // TODO: 받아온 값으로 수정 +// guard let review = productDetail.reviews.first(where: { $0.reviewId == reviewId }) else { return } + router?.attachMyDetailReview(productDetail: productDetail, review: .init(reviewId: "", taste: .good, quality: .bad, valueForMoney: .normal, score: 0, contents: "설명입니다 설명입니다 설명입니다 설명입니다 설명입니다 설명입니다설명입니다 설명입니다 설명입니다", image: nil, writerId: nil, writerName: "양볼 빵빵 다람쥐", createdTime: "", updatedTime: "", likeCount: .init(writerIds: [], likeCount: 0), hateCount: .init(writerIds: [], hateCount: 0))) + } + + func didTapBackButton() { + router?.detachMyDetailReview() + } + + func didTapMyReviewBackButton() { + listener?.detachMyReview() + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewRouter.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewRouter.swift new file mode 100644 index 00000000..927c785b --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewRouter.swift @@ -0,0 +1,54 @@ +// +// MyReviewRouter.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs + +protocol MyReviewInteractable: Interactable, + MyDetailReviewListener { + var router: MyReviewRouting? { get set } + var listener: MyReviewListener? { get set } +} + +protocol MyReviewViewControllable: ViewControllable { + // TODO: Declare methods the router invokes to manipulate the view hierarchy. +} + +final class MyReviewRouter: ViewableRouter, MyReviewRouting { + + + private let myDetailReviewBuilder: MyDetailReviewBuildable + private var myDetailReviewRouting: ViewableRouting? + + init( + interactor: MyReviewInteractable, + viewController: MyReviewViewControllable, + myDetailReviewBuilder: MyDetailReviewBuilder + ) { + self.myDetailReviewBuilder = myDetailReviewBuilder + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + func attachMyDetailReview(productDetail: ProductDetailEntity, review: ReviewEntity) { + if myDetailReviewRouting != nil { return } + let myDetailReviewRouter = myDetailReviewBuilder.build( + withListener: interactor, + productDetail: productDetail, + review: review + ) + myDetailReviewRouting = myDetailReviewRouter + attachChild(myDetailReviewRouter) + viewController.pushViewController(myDetailReviewRouter.viewControllable, animated: true) + } + + func detachMyDetailReview() { + guard let myDetailReviewRouting else { return } + viewController.popViewController(animated: true) + self.myDetailReviewRouting = nil + detachChild(myDetailReviewRouting) + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewViewController.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewViewController.swift new file mode 100644 index 00000000..02b4d01e --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewViewController.swift @@ -0,0 +1,96 @@ +// +// MyReviewViewController.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import ModernRIBs +import UIKit + +protocol MyReviewPresentableListener: AnyObject { + func didTapMyReviewBackButton() + func didTapMyDetailReview(with productDetail: ProductDetailEntity, reviewId: String) +} + +final class MyReviewViewController: UIViewController, + MyReviewPresentable, + MyReviewViewControllable { + + weak var listener: MyReviewPresentableListener? + var viewHolder: MyReviewViewController.ViewHolder = .init() + + override func viewDidLoad() { + super.viewDidLoad() + viewHolder.place(in: view) + viewHolder.configureConstraints(for: view) + self.configureUI() + self.bindActions() + self.configureTableView() + } + + func update(reviews: [ReviewEntity]) { + self.updateNavigationTitle(reviewCount: reviews.count) + } + + // MARK: - Private Mehods + private func configureUI() { + self.view.backgroundColor = .white + } + + private func bindActions() { + self.viewHolder.backNavigationView.delegate = self + } + + private func updateNavigationTitle(reviewCount: Int) { + let updatedTitle = Text.navigationTitleView + "(\(reviewCount))" + self.viewHolder.backNavigationView.setText(with: updatedTitle) + } + + private func configureTableView() { + self.viewHolder.tableView.register( + UITableViewCell.self, + forCellReuseIdentifier: MyReviewContentView.identifier + ) + self.viewHolder.tableView.delegate = self + self.viewHolder.tableView.dataSource = self + } +} + +// MARK: - UITableViewDelegate +extension MyReviewViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + listener?.didTapMyDetailReview(with: ProductDetailEntity(id: "", storeType: .cu, imageURL: URL(string: "www.naver.com")!, name: "", price: "", eventType: nil, productType: .event, updatedTime: "", description: nil, isNew: nil, viewCount: 0, category: nil, isFavorite: nil, originPrice: nil, giftImageURL: nil, giftTitle: nil, giftPrice: nil, isEventExpired: nil, reviews: [], avgScore: nil), reviewId: "1") // TODO: interactor로부터 받아온 값으로 변경 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } +} + +// MARK: - UITableViewDataSource +extension MyReviewViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 10 // TODO: interactor로부터 받아온 값으로 변경 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: MyReviewContentView.identifier, for: indexPath) + cell.selectionStyle = .none // TODO: interactor로부터 받아온 값으로 변경 + cell.contentConfiguration = MyReviewContentConfiguration( + mode: .date, + storeImageIcon: .sevenEleven, + imageUrl: URL(string: "www.naver.com")!, + title: "테스트", + date: "2024.05.19" + ) + return cell + } +} + +// MARK: - BackNavigationViewDelegate +extension MyReviewViewController: BackNavigationViewDelegate { + func didTapBackButton() { + listener?.didTapMyReviewBackButton() + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewViewHolder.swift new file mode 100644 index 00000000..b9ec02df --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/MyReviewViewHolder.swift @@ -0,0 +1,46 @@ +// +// MyReviewViewHolder.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import UIKit + +extension MyReviewViewController { + enum Text { + static let navigationTitleView = "내 리뷰" + } + + class ViewHolder: ViewHolderable { + let backNavigationView: BackNavigationView = { + let navigationView = BackNavigationView() + navigationView.payload = .init(mode: .text) + navigationView.setText(with: Text.navigationTitleView) + return navigationView + }() + + let tableView: UITableView = { + let tableView = UITableView() + return tableView + }() + + func place(in view: UIView) { + view.addSubview(backNavigationView) + view.addSubview(tableView) + } + + func configureConstraints(for view: UIView) { + backNavigationView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.leading.trailing.equalTo(view) + } + + tableView.snp.makeConstraints { + $0.top.equalTo(backNavigationView.snp.bottom) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/View/MyReviewContentConfiguration.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/View/MyReviewContentConfiguration.swift new file mode 100644 index 00000000..a50b274e --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/View/MyReviewContentConfiguration.swift @@ -0,0 +1,41 @@ +// +// MyReviewContentConfiguration.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import UIKit + +struct MyReviewContentConfiguration: UIContentConfiguration { + var storeImageIcon: ConvenienceStore + var imageUrl: URL + var title: String + var date: String? + var mode: ProductInfoStackView.Mode + var tastesTag: [String]? + + init( + mode: ProductInfoStackView.Mode, + storeImageIcon: ConvenienceStore, + imageUrl: URL, + title: String, + date: String? = nil, + tastesTag: [String]? = nil + ) { + self.mode = mode + self.storeImageIcon = storeImageIcon + self.imageUrl = imageUrl + self.title = title + self.date = date + self.tastesTag = tastesTag + } + + func makeContentView() -> any UIView & UIContentView { + return MyReviewContentView(configuration: self, mode: mode) + } + + func updated(for state: any UIConfigurationState) -> MyReviewContentConfiguration { + return self + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/MyReview/View/MyReviewContentView.swift b/Pyonsnal-Color/Pyonsnal-Color/MyReview/View/MyReviewContentView.swift new file mode 100644 index 00000000..31fd3179 --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/MyReview/View/MyReviewContentView.swift @@ -0,0 +1,58 @@ +// +// MyReviewContentView.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import UIKit +import SnapKit + +class MyReviewContentView: UIView, UIContentView { + static let identifier = "MyReviewContentView" + + var configuration: any UIContentConfiguration + private let storeImageViewHeight: CGFloat = 20 + + private var productInfoStackView: ProductInfoStackView + + init(configuration: any UIContentConfiguration, mode: ProductInfoStackView.Mode) { + self.configuration = configuration + self.productInfoStackView = ProductInfoStackView(mode: mode) + super.init(frame: .zero) + configureView() + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureView() { + self.addSubview(productInfoStackView) + productInfoStackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + private func configureUI() { + guard let configuration = configuration as? MyReviewContentConfiguration else { return } + self.productInfoStackView.productImageView.setImage(with: configuration.imageUrl) + self.productInfoStackView.productNameLabel.text = configuration.title + self.productInfoStackView.storeImageView.setImage(configuration.storeImageIcon.storeIcon) + self.productInfoStackView.storeImageView.snp.makeConstraints { make in + guard let storeIcon = configuration.storeImageIcon.storeIcon.image else { return } + let ratio = storeIcon.size.width / storeIcon.size.height + let newWidth = self.storeImageViewHeight * ratio + make.width.equalTo(newWidth) + } + + if let date = configuration.date { + self.productInfoStackView.dateLabel.text = configuration.date + } + + if let tastesTag = configuration.tastesTag { + self.productInfoStackView.setTastes(tastesTag: tastesTag) + } + } +} diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/ProductDetailViewController.swift b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/ProductDetailViewController.swift index 5109227a..7a730884 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/ProductDetailViewController.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/ProductDetailViewController.swift @@ -180,6 +180,10 @@ extension ProductDetailViewController: UICollectionViewDataSource { ) reviewWriteCell.delegate = self return reviewWriteCell + case .actionButton: + let actionButtonCell: ActionButtonCell = collectionView.dequeueReusableCell(for: indexPath) + actionButtonCell.actionButton.setText(with: ActionButtonCell.Constants.Text.writeReview) + return actionButtonCell case let .review(entity): let reviewCell: ProductDetailReviewCell = collectionView.dequeueReusableCell( for: indexPath @@ -236,7 +240,9 @@ extension ProductDetailViewController: UICollectionViewDelegateFlowLayout { ) return .init(width: screenWidth, height: estimateSize.height) case .reviewWrite: - return .init(width: screenWidth, height: 300) + return .init(width: screenWidth, height: 114) + case .actionButton: + return .init(width: screenWidth, height: 96) case let .review(entity): let width = collectionView.bounds.size.width let estimateHeight: CGFloat = 1000 diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewEvaluationLabelView/ReviewEvaluationLabelView.swift b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewEvaluationLabelView/ReviewEvaluationLabelView.swift index ad323ba3..328e8146 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewEvaluationLabelView/ReviewEvaluationLabelView.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewEvaluationLabelView/ReviewEvaluationLabelView.swift @@ -81,7 +81,6 @@ final class ReviewEvaluationLabelView: UIView { contentStackView.addArrangedSubview(evaluationLabel) contentView.snp.makeConstraints { make in - make.height.equalTo(28) make.edges.equalToSuperview() } @@ -91,12 +90,10 @@ final class ReviewEvaluationLabelView: UIView { } categoryLabel.snp.makeConstraints { make in - make.height.equalTo(20) make.top.bottom.equalToSuperview() } evaluationLabel.snp.makeConstraints { make in - make.height.equalTo(20) make.top.bottom.equalToSuperview() } } diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell+ViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell+ViewHolder.swift index 0fa6721c..ce60ad62 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell+ViewHolder.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell+ViewHolder.swift @@ -81,15 +81,6 @@ extension ProductDetailReviewWriteCell { return starRatedView }() - let reviewWriteButton: UIButton = { - let button = UIButton(frame: .zero) - button.makeRounded(with: Size.writeButtonRadius) - button.backgroundColor = .black - button.titleLabel?.textColor = .white - button.titleLabel?.font = .body2m - return button - }() - // MARK: - Interface func place(in view: UIView) { view.addSubview(contentView) @@ -97,7 +88,6 @@ extension ProductDetailReviewWriteCell { contentView.addSubview(reviewCountLabel) contentView.addSubview(sortButton) contentView.addSubview(ratingBackgroundView) - contentView.addSubview(reviewWriteButton) ratingBackgroundView.addSubview(ratingScoreStackView) ratingBackgroundView.addSubview(starRatedView) @@ -140,12 +130,6 @@ extension ProductDetailReviewWriteCell { make.bottom.equalToSuperview().inset(.spacing24) } - reviewWriteButton.snp.makeConstraints { make in - make.height.equalTo(40) - make.top.equalTo(ratingBackgroundView.snp.bottom).offset(.spacing16) - make.leading.trailing.equalToSuperview().inset(.spacing16) - make.bottom.equalToSuperview().inset(.spacing40) - } } } } diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell.swift b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell.swift index 5292578f..ee07fd42 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCell.swift @@ -49,10 +49,6 @@ final class ProductDetailReviewWriteCell: UICollectionViewCell { } private func configureAction() { - viewHolder.reviewWriteButton.addTapGesture( - target: self, - action: #selector(reviewWriteButtonAction(_:)) - ) viewHolder.sortButton.addTapGesture( target: self, @@ -68,14 +64,9 @@ final class ProductDetailReviewWriteCell: UICollectionViewCell { viewHolder.reviewCountLabel.text = "리뷰 \(payload.reviewsCount)개" viewHolder.ratingScoreLabel.text = "\(round(payload.score * 10) / 10)" viewHolder.starRatedView.payload = .init(score: payload.score) - viewHolder.reviewWriteButton.setText(with: "상품 리뷰 작성 하기") viewHolder.sortButton.setText(with: payload.sortItem.name) } - @objc private func reviewWriteButtonAction(_ sender: UITapGestureRecognizer) { - delegate?.writeButtonDidTap() - } - @objc private func reviewSortButtonAction(_ sender: UITapGestureRecognizer) { delegate?.sortButtonDidTap() } diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCellDelegate.swift b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCellDelegate.swift index d1a19e7e..b32f6828 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCellDelegate.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProductDetail/Subview/ReviewWriteCell/ProductDetailReviewWriteCellDelegate.swift @@ -6,6 +6,5 @@ // protocol ProductDetailReviewWriteCellDelegate: AnyObject { - func writeButtonDidTap() func sortButtonDidTap() } diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeBuilder.swift b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeBuilder.swift index 00a9025c..0d833b88 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeBuilder.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeBuilder.swift @@ -14,6 +14,7 @@ final class ProfileHomeComponent: Component, ProfileEditDependency, AccountSettingDependency, CommonWebDependency, + MyReviewDependency, LoggedOutDependency { var appleLoginService: AppleLoginService var kakaoLoginService: KakaoLoginService @@ -57,6 +58,7 @@ final class ProfileHomeBuilder: Builder, let profileEditBuilder = ProfileEditBuilder(dependency: component) let accountSettingBuilder = AccountSettingBuilder(dependency: component) let commonWebBuilder = CommonWebBuilder(dependency: component) + let myReviewBuilder = MyReviewBuilder(dependency: component) let loggedOutBuilder = LoggedOutBuilder(dependency: component) return ProfileHomeRouter( @@ -65,6 +67,7 @@ final class ProfileHomeBuilder: Builder, profileEditBuilder: profileEditBuilder, accountSettingBuilder: accountSettingBuilder, commonWebBuilder: commonWebBuilder, + myReviewBuilder: myReviewBuilder, loggedOutBuilder: loggedOutBuilder ) } diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeInteractor.swift b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeInteractor.swift index d1a62047..c9675fe4 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeInteractor.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeInteractor.swift @@ -18,6 +18,9 @@ protocol ProfileHomeRouting: ViewableRouting { func attachAccountSetting() func detachAccountSetting() + func attachMyReview() + func detachMyReview() + func attachLoggedOut() func detachLoggedOut() } @@ -67,6 +70,10 @@ final class ProfileHomeInteractor: PresentableInteractor }.store(in: &cancellable) } + func didTapMyReview() { + router?.attachMyReview() + } + func didTapAccountSetting() { router?.attachAccountSetting() } @@ -111,4 +118,8 @@ final class ProfileHomeInteractor: PresentableInteractor router?.detachLoggedOut() } + func detachMyReview() { + router?.detachMyReview() + } + } diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeRouter.swift b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeRouter.swift index 186ba899..4ecf4d9b 100644 --- a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeRouter.swift +++ b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeRouter.swift @@ -11,6 +11,7 @@ protocol ProfileHomeInteractable: Interactable, ProfileEditListener, AccountSettingListener, CommonWebListener, + MyReviewListener, LoggedOutListener { var router: ProfileHomeRouting? { get set } var listener: ProfileHomeListener? { get set } @@ -35,6 +36,9 @@ final class ProfileHomeRouter: ViewableRouter() private var memberInfo: MemberInfoEntity? - private let sections: [Section] = [.setting] + private let sections: [Section] = [.review, .setting] + private var reviews: [Review] = [.review, .myReview] private var settings: [Setting] = [.etc, .email, .version, .teams, .account] // MARK: - Initializer @@ -119,10 +125,17 @@ final class ProfileHomeViewController: UIViewController, guard let self, let memberInfo else { return } self.listener?.didTapProfileEditButton(memberInfo: memberInfo) }.store(in: &cancellable) + + viewHolder.charactorLandingButton + .tapPublisher + .throttle(for: 0.5, scheduler: RunLoop.main, latest: false) + .sink { [weak self] _ in + guard let self else { return } + // TODO: Landing to charactor page + }.store(in: &cancellable) } private func configureTabBarItem() { - tabBarItem.setTitleTextAttributes([.font: UIFont.label2], for: .normal) tabBarItem = UITabBarItem( title: "마이페이지", @@ -162,6 +175,8 @@ extension ProfileHomeViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let section = sections[section] switch section { + case .review: + return reviews.count case .setting: return settings.count } @@ -173,11 +188,15 @@ extension ProfileHomeViewController: UITableViewDataSource { let section = sections[indexPath.section] let isSectionIndex = isSectionIndex(with: indexPath.row) let isSubLabelToShow = isSubLabelToShow(section: section, index: indexPath.row) + let title: String + switch section { + case .review: + title = reviews[indexPath.row].title case .setting: - cell.update(text: settings[indexPath.row].title, - isSectionIndex: isSectionIndex) + title = settings[indexPath.row].title } + cell.update(text: title, isSectionIndex: isSectionIndex) cell.setSubLabelHidden(isShow: isSubLabelToShow) return cell } @@ -187,7 +206,7 @@ extension ProfileHomeViewController: UITableViewDataSource { extension ProfileHomeViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let defaultHeight = Size.cellHeight + let defaultHeight: CGFloat = 48 return defaultHeight } @@ -195,6 +214,10 @@ extension ProfileHomeViewController: UITableViewDelegate { let section = sections[indexPath.section] if !isSectionIndex(with: indexPath.row) { switch section { + case .review: + if indexPath.row == Review.myReview.rawValue { + listener?.didTapMyReview() + } case .setting: if indexPath.row == Setting.email.rawValue { showEmailReport() @@ -211,100 +234,7 @@ extension ProfileHomeViewController: UITableViewDelegate { } } -// MARK: - UI Component -extension ProfileHomeViewController { - class ViewHolder: ViewHolderable { - - private let profileContainerView: UIView = { - let view = UIView() - view.backgroundColor = .white - return view - }() - - let profileImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - imageView.makeRounded(with: Size.profileImageViewSize / 2) - imageView.setImage(.tagStore) - return imageView - }() - - let nickNameLabel: UILabel = { - let label = UILabel() - label.font = .title2 - label.textColor = .black - label.numberOfLines = 1 - return label - }() - - let profileEditButton: UIButton = { - let button = UIButton() - button.setImage(ImageAssetKind.Profile.profileEdit.image, for: .normal) - return button - }() - - let tableView: UITableView = { - let tableView = UITableView() - tableView.separatorStyle = .none - tableView.bounces = false - return tableView - }() - - private let dividerView: UIView = { - let view = UIView() - view.backgroundColor = .gray100 - return view - }() - - func place(in view: UIView) { - view.addSubview(profileContainerView) - profileContainerView.addSubview(profileImageView) - profileContainerView.addSubview(nickNameLabel) - profileContainerView.addSubview(profileEditButton) - view.addSubview(dividerView) - view.addSubview(tableView) - } - - func configureConstraints(for view: UIView) { - profileContainerView.snp.makeConstraints { - $0.top.equalTo(view.safeAreaLayoutGuide.snp.top) - $0.leading.trailing.equalToSuperview() - $0.height.equalTo(Size.profileContainerViewHeight) - } - - profileImageView.snp.makeConstraints { - $0.size.equalTo(Size.profileImageViewSize) - $0.leading.equalToSuperview().offset(Size.profileImageViewLeading) - $0.centerY.equalToSuperview() - } - - nickNameLabel.snp.makeConstraints { - $0.leading.equalTo(profileImageView.snp.trailing).offset(.spacing12) - $0.centerY.equalTo(profileContainerView.snp.centerY) - $0.trailing.greaterThanOrEqualTo(profileEditButton.snp.leading).inset(.spacing12) - } - - profileEditButton.snp.makeConstraints { - $0.top.equalToSuperview().offset(.spacing28) - $0.trailing.equalToSuperview().inset(.spacing4) - $0.size.equalTo(Size.profileEditButtonSize) - } - - dividerView.snp.makeConstraints { - $0.top.equalTo(profileContainerView.snp.bottom) - $0.leading.trailing.equalToSuperview() - $0.height.equalTo(Size.dividerMargin) - } - - tableView.snp.makeConstraints { - $0.top.equalTo(dividerView.snp.bottom) - $0.leading.trailing.bottom.equalToSuperview() - } - - } - } -} - +// MARK: - MFMailComposeViewControllerDelegate extension ProfileHomeViewController: MFMailComposeViewControllerDelegate { func mailComposeController( _ controller: MFMailComposeViewController, diff --git a/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeViewHolder.swift b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeViewHolder.swift new file mode 100644 index 00000000..2449d84b --- /dev/null +++ b/Pyonsnal-Color/Pyonsnal-Color/ProfileHome/ProfileHomeViewHolder.swift @@ -0,0 +1,123 @@ +// +// ProfileHomeViewHolder.swift +// Pyonsnal-Color +// +// Created by 조소정 on 5/19/24. +// + +import UIKit +import SnapKit + +// MARK: - UI Component +extension ProfileHomeViewController { + class ViewHolder: ViewHolderable { + + enum Size { + static let profileImageViewSize: CGFloat = 40 + static let profileEditButtonSize: CGFloat = 48 + static let profileImageViewLeading: CGFloat = 17 + static let profileContainerViewHeight: CGFloat = 104 + static let charactorSlotViewHeight: CGFloat = 152 + } + + private let profileContainerView: UIView = { + let view = UIView() + view.backgroundColor = .white + return view + }() + + let profileImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.makeRounded(with: Size.profileImageViewSize / 2) + imageView.setImage(.tagStore) + return imageView + }() + + let nickNameLabel: UILabel = { + let label = UILabel() + label.font = .title2 + label.textColor = .black + label.numberOfLines = 1 + return label + }() + + let profileEditButton: UIButton = { + let button = UIButton() + button.setImage(ImageAssetKind.Profile.profileEdit.image, for: .normal) + return button + }() + + let tableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.bounces = false + return tableView + }() + + private let charactorSlotView: UIView = { + let view = UIView() + view.backgroundColor = .gray100 + return view + }() + + let charactorLandingButton: UIButton = { + let button = UIButton() + button.backgroundColor = .gray400 + return button + }() + + func place(in view: UIView) { + view.addSubview(profileContainerView) + profileContainerView.addSubview(profileImageView) + profileContainerView.addSubview(nickNameLabel) + profileContainerView.addSubview(profileEditButton) + view.addSubview(charactorSlotView) + charactorSlotView.addSubview(charactorLandingButton) + view.addSubview(tableView) + } + + func configureConstraints(for view: UIView) { + profileContainerView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide.snp.top) + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(Size.profileContainerViewHeight) + } + + profileImageView.snp.makeConstraints { + $0.size.equalTo(Size.profileImageViewSize) + $0.leading.equalToSuperview().offset(Size.profileImageViewLeading) + $0.centerY.equalToSuperview() + } + + nickNameLabel.snp.makeConstraints { + $0.leading.equalTo(profileImageView.snp.trailing).offset(.spacing12) + $0.centerY.equalTo(profileContainerView.snp.centerY) + $0.trailing.greaterThanOrEqualTo(profileEditButton.snp.leading).inset(.spacing12) + } + + profileEditButton.snp.makeConstraints { + $0.top.equalToSuperview().offset(.spacing28) + $0.trailing.equalToSuperview().inset(.spacing4) + $0.size.equalTo(Size.profileEditButtonSize) + } + + charactorSlotView.snp.makeConstraints { + $0.top.equalTo(profileContainerView.snp.bottom) + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(Size.charactorSlotViewHeight) + } + + charactorLandingButton.snp.makeConstraints { + $0.top.leading.equalToSuperview().offset(10) + $0.trailing.bottom.equalToSuperview().inset(10) + } + + tableView.snp.makeConstraints { + $0.top.equalTo(charactorSlotView.snp.bottom) + $0.leading.trailing.bottom.equalToSuperview() + } + + } + } +}