diff --git a/SOOUM/SOOUM.xcodeproj/project.pbxproj b/SOOUM/SOOUM.xcodeproj/project.pbxproj index 0b279b2a..295920ee 100644 --- a/SOOUM/SOOUM.xcodeproj/project.pbxproj +++ b/SOOUM/SOOUM.xcodeproj/project.pbxproj @@ -10,10 +10,6 @@ 2192C5356D8F39905D0A8181 /* Pods_SOOUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74194BC62F22BC2F5596D850 /* Pods_SOOUM.framework */; }; 2A032EFD2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A032EFC2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift */; }; 2A032EFE2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A032EFC2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift */; }; - 2A048E7B2C9BDF5F00FFD485 /* SOMLocationFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A048E7A2C9BDF5F00FFD485 /* SOMLocationFilter.swift */; }; - 2A048E7C2C9BDF5F00FFD485 /* SOMLocationFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A048E7A2C9BDF5F00FFD485 /* SOMLocationFilter.swift */; }; - 2A048E842C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A048E832C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift */; }; - 2A048E852C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A048E832C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift */; }; 2A34AFB52D144F08007BD7E7 /* EmptyTagDetailTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A34AFB42D144EEF007BD7E7 /* EmptyTagDetailTableViewCell.swift */; }; 2A34AFB62D144F08007BD7E7 /* EmptyTagDetailTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A34AFB42D144EEF007BD7E7 /* EmptyTagDetailTableViewCell.swift */; }; 2A44A42A2CAC09AE00DC463E /* RSAKeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A44A4292CAC09AE00DC463E /* RSAKeyResponse.swift */; }; @@ -165,6 +161,24 @@ 3803CF862D017DC700FD90DB /* EnterMemberTransferViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3803CF842D017DC700FD90DB /* EnterMemberTransferViewReactor.swift */; }; 3803CF882D01914200FD90DB /* ResignViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3803CF872D01914200FD90DB /* ResignViewReactor.swift */; }; 3803CF892D01914200FD90DB /* ResignViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3803CF872D01914200FD90DB /* ResignViewReactor.swift */; }; + 380F42212E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42202E87ECA2009AC59E /* CompositeNotificationInfo.swift */; }; + 380F42222E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42202E87ECA2009AC59E /* CompositeNotificationInfo.swift */; }; + 380F42242E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42232E884ADF009AC59E /* CardRemoteDataSource.swift */; }; + 380F42252E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42232E884ADF009AC59E /* CardRemoteDataSource.swift */; }; + 380F42272E884B80009AC59E /* BaseCardInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42262E884B6F009AC59E /* BaseCardInfo.swift */; }; + 380F42282E884B80009AC59E /* BaseCardInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42262E884B6F009AC59E /* BaseCardInfo.swift */; }; + 380F422A2E884E9C009AC59E /* HomeCardInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42292E884E85009AC59E /* HomeCardInfoResponse.swift */; }; + 380F422B2E884E9C009AC59E /* HomeCardInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42292E884E85009AC59E /* HomeCardInfoResponse.swift */; }; + 380F422D2E884F3D009AC59E /* CardRemoteDataSourceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F422C2E884F35009AC59E /* CardRemoteDataSourceImpl.swift */; }; + 380F422E2E884F3D009AC59E /* CardRemoteDataSourceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F422C2E884F35009AC59E /* CardRemoteDataSourceImpl.swift */; }; + 380F42302E884FBC009AC59E /* CardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F422F2E884FB5009AC59E /* CardRepository.swift */; }; + 380F42312E884FBC009AC59E /* CardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F422F2E884FB5009AC59E /* CardRepository.swift */; }; + 380F42332E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */; }; + 380F42342E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */; }; + 380F42362E885032009AC59E /* CardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42352E88502F009AC59E /* CardUseCase.swift */; }; + 380F42372E885032009AC59E /* CardUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42352E88502F009AC59E /* CardUseCase.swift */; }; + 380F42392E88505B009AC59E /* CardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42382E885057009AC59E /* CardUseCaseImpl.swift */; }; + 380F423A2E88505B009AC59E /* CardUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F42382E885057009AC59E /* CardUseCaseImpl.swift */; }; 38121E292CA6A52400602499 /* UIRefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38121E282CA6A52400602499 /* UIRefreshControl.swift */; }; 38121E2A2CA6A52400602499 /* UIRefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38121E282CA6A52400602499 /* UIRefreshControl.swift */; }; 38121E312CA6C77500602499 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38121E302CA6C77500602499 /* Double.swift */; }; @@ -199,14 +213,10 @@ 381DEA8E2CD4BE55009F1FE9 /* SignInResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A44A42C2CAC14C800DC463E /* SignInResponse.swift */; }; 382D5CF62CFE9B8600BFA23E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382D5CF52CFE9B8600BFA23E /* ProfileViewController.swift */; }; 382D5CF72CFE9B8600BFA23E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382D5CF52CFE9B8600BFA23E /* ProfileViewController.swift */; }; - 382E15362D15A6460097B09C /* NotificationTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E15352D15A6460097B09C /* NotificationTabBarController.swift */; }; - 382E15372D15A6460097B09C /* NotificationTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E15352D15A6460097B09C /* NotificationTabBarController.swift */; }; 382E153A2D15A67A0097B09C /* NotificationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E15392D15A67A0097B09C /* NotificationViewCell.swift */; }; 382E153B2D15A67A0097B09C /* NotificationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E15392D15A67A0097B09C /* NotificationViewCell.swift */; }; 382E153F2D15AF4F0097B09C /* CommentHistoryInNotiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E153E2D15AF4F0097B09C /* CommentHistoryInNotiResponse.swift */; }; 382E15402D15AF4F0097B09C /* CommentHistoryInNotiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E153E2D15AF4F0097B09C /* CommentHistoryInNotiResponse.swift */; }; - 382E15422D15BA490097B09C /* NotificationWithReportViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E15412D15BA490097B09C /* NotificationWithReportViewCell.swift */; }; - 382E15432D15BA490097B09C /* NotificationWithReportViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382E15412D15BA490097B09C /* NotificationWithReportViewCell.swift */; }; 3830FFA62CEC6E3100ABA9FD /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3830FFA52CEC6E3100ABA9FD /* Kingfisher.swift */; }; 3830FFA72CEC6E3100ABA9FD /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3830FFA52CEC6E3100ABA9FD /* Kingfisher.swift */; }; 3834FADD2D11C5AC00C9108D /* SimpleCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3834FADC2D11C5AC00C9108D /* SimpleCache.swift */; }; @@ -258,8 +268,8 @@ 385441922C870544004E2BB0 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387FBAF12C8702C100A5E139 /* SceneDelegate.swift */; }; 385441952C870544004E2BB0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 387FBAF82C8702C200A5E139 /* Assets.xcassets */; }; 385441962C870544004E2BB0 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 387FBAFB2C8702C200A5E139 /* Base */; }; - 385602B62D2FB18400118530 /* NotiPlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385602B52D2FB18400118530 /* NotiPlaceholderViewCell.swift */; }; - 385602B72D2FB18400118530 /* NotiPlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385602B52D2FB18400118530 /* NotiPlaceholderViewCell.swift */; }; + 385602B62D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385602B52D2FB18400118530 /* NotificationPlaceholderViewCell.swift */; }; + 385602B72D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385602B52D2FB18400118530 /* NotificationPlaceholderViewCell.swift */; }; 385620EF2CA19C9500E0AB5A /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385620EE2CA19C9500E0AB5A /* NetworkManager.swift */; }; 385620F02CA19C9500E0AB5A /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385620EE2CA19C9500E0AB5A /* NetworkManager.swift */; }; 385620F22CA19D2D00E0AB5A /* Alamofire_Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385620F12CA19D2D00E0AB5A /* Alamofire_Request.swift */; }; @@ -270,10 +280,18 @@ 38572CD92D2230C900B07C69 /* NotificationAllowResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CD72D2230C900B07C69 /* NotificationAllowResponse.swift */; }; 38572CDB2D22464F00B07C69 /* PungTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CDA2D22464F00B07C69 /* PungTimeView.swift */; }; 38572CDC2D22464F00B07C69 /* PungTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CDA2D22464F00B07C69 /* PungTimeView.swift */; }; - 38572CDE2D2254E800B07C69 /* PlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CDD2D2254E800B07C69 /* PlaceholderViewCell.swift */; }; - 38572CDF2D2254E800B07C69 /* PlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CDD2D2254E800B07C69 /* PlaceholderViewCell.swift */; }; + 38572CDE2D2254E800B07C69 /* HomePlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CDD2D2254E800B07C69 /* HomePlaceholderViewCell.swift */; }; + 38572CDF2D2254E800B07C69 /* HomePlaceholderViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38572CDD2D2254E800B07C69 /* HomePlaceholderViewCell.swift */; }; 3857BC3F2D4D1FFA008D4264 /* MockManagerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3857BC3E2D4D1FFA008D4264 /* MockManagerProvider.swift */; }; 3857BC412D4D22B4008D4264 /* MockManagerProviderContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3857BC402D4D22B4008D4264 /* MockManagerProviderContainerTests.swift */; }; + 385C01AE2E8E8C6F003C7894 /* SOMPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01AD2E8E8C6A003C7894 /* SOMPageModel.swift */; }; + 385C01AF2E8E8C6F003C7894 /* SOMPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01AD2E8E8C6A003C7894 /* SOMPageModel.swift */; }; + 385C01B12E8E8DD8003C7894 /* SOMPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01B02E8E8DD4003C7894 /* SOMPageView.swift */; }; + 385C01B22E8E8DD8003C7894 /* SOMPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01B02E8E8DD4003C7894 /* SOMPageView.swift */; }; + 385C01B42E8EA1B7003C7894 /* SOMPageViewsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01B32E8EA1B1003C7894 /* SOMPageViewsDelegate.swift */; }; + 385C01B52E8EA1B7003C7894 /* SOMPageViewsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01B32E8EA1B1003C7894 /* SOMPageViewsDelegate.swift */; }; + 385C01B72E8EA1EF003C7894 /* SOMPageViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01B62E8EA1EB003C7894 /* SOMPageViews.swift */; }; + 385C01B82E8EA1EF003C7894 /* SOMPageViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C01B62E8EA1EB003C7894 /* SOMPageViews.swift */; }; 385E65A32CBE56D00032E120 /* Coordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385E65A22CBE56D00032E120 /* Coordinate.swift */; }; 385E65A42CBE56D00032E120 /* Coordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385E65A22CBE56D00032E120 /* Coordinate.swift */; }; 38601E182D31399400A465A9 /* CardRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384972A02CA4DEC00012FCA1 /* CardRequest.swift */; }; @@ -338,8 +356,8 @@ 3878F4772CA3F08300AA46A2 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3878F4762CA3F08300AA46A2 /* UIView.swift */; }; 3878F4782CA3F08300AA46A2 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3878F4762CA3F08300AA46A2 /* UIView.swift */; }; 3878FE0D2D0365C800D8955C /* SOMNavigationBar+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3878FE0C2D0365C800D8955C /* SOMNavigationBar+Rx.swift */; }; - 387D852C2D08320A005D9D22 /* SOMCardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387D852B2D08320A005D9D22 /* SOMCardModel.swift */; }; - 387D852D2D08320A005D9D22 /* SOMCardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387D852B2D08320A005D9D22 /* SOMCardModel.swift */; }; + 387FA11D2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387FA11C2E88DDBD004DF7CE /* HomeViewReactor.swift */; }; + 387FA11E2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387FA11C2E88DDBD004DF7CE /* HomeViewReactor.swift */; }; 387FBAF02C8702C100A5E139 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387FBAEF2C8702C100A5E139 /* AppDelegate.swift */; }; 387FBAF22C8702C100A5E139 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 387FBAF12C8702C100A5E139 /* SceneDelegate.swift */; }; 387FBAF92C8702C200A5E139 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 387FBAF82C8702C200A5E139 /* Assets.xcassets */; }; @@ -366,18 +384,12 @@ 388372022C8C8FCF004212EB /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388372002C8C8FCF004212EB /* UIColor.swift */; }; 3886939F2CF77FA7005F9EF3 /* UIApplication+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3886939E2CF77FA7005F9EF3 /* UIApplication+Top.swift */; }; 388693A02CF77FA7005F9EF3 /* UIApplication+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3886939E2CF77FA7005F9EF3 /* UIApplication+Top.swift */; }; - 388698512D191F2100008600 /* MainHomeDistanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698502D191F2100008600 /* MainHomeDistanceViewController.swift */; }; - 388698522D191F2100008600 /* MainHomeDistanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698502D191F2100008600 /* MainHomeDistanceViewController.swift */; }; - 388698542D191F4B00008600 /* MainHomeDistanceViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698532D191F4B00008600 /* MainHomeDistanceViewReactor.swift */; }; - 388698552D191F4B00008600 /* MainHomeDistanceViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698532D191F4B00008600 /* MainHomeDistanceViewReactor.swift */; }; 388698582D1982DE00008600 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698572D1982DE00008600 /* NotificationViewController.swift */; }; 388698592D1982DE00008600 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698572D1982DE00008600 /* NotificationViewController.swift */; }; 3886985F2D1984D600008600 /* NotificationViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3886985E2D1984D600008600 /* NotificationViewReactor.swift */; }; 388698602D1984D600008600 /* NotificationViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3886985E2D1984D600008600 /* NotificationViewReactor.swift */; }; 388698622D1986B100008600 /* NotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698612D1986B100008600 /* NotificationRequest.swift */; }; 388698632D1986B100008600 /* NotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698612D1986B100008600 /* NotificationRequest.swift */; }; - 388698652D1998DB00008600 /* NotificationTabBarReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698642D1998DB00008600 /* NotificationTabBarReactor.swift */; }; - 388698662D1998DB00008600 /* NotificationTabBarReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388698642D1998DB00008600 /* NotificationTabBarReactor.swift */; }; 3887176A2E7BD7AC00C6143B /* loading_indicator_lottie.json in Resources */ = {isa = PBXBuildFile; fileRef = 388717692E7BD7AC00C6143B /* loading_indicator_lottie.json */; }; 3887176B2E7BD7AC00C6143B /* loading_indicator_lottie.json in Resources */ = {isa = PBXBuildFile; fileRef = 388717692E7BD7AC00C6143B /* loading_indicator_lottie.json */; }; 3887176D2E7BDBAE00C6143B /* NicknameResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3887176C2E7BDBA800C6143B /* NicknameResponse.swift */; }; @@ -400,8 +412,6 @@ 38899E6F2E79400C0030F7CA /* ImageUrlInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E6D2E79400B0030F7CA /* ImageUrlInfoResponse.swift */; }; 38899E712E79402C0030F7CA /* ImageUrlInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E702E7940280030F7CA /* ImageUrlInfo.swift */; }; 38899E722E79402C0030F7CA /* ImageUrlInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E702E7940280030F7CA /* ImageUrlInfo.swift */; }; - 38899E742E79430A0030F7CA /* SignUpRequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E732E7943070030F7CA /* SignUpRequestInfo.swift */; }; - 38899E752E79430A0030F7CA /* SignUpRequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E732E7943070030F7CA /* SignUpRequestInfo.swift */; }; 38899E7D2E794B420030F7CA /* SignUpResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E7C2E794B3D0030F7CA /* SignUpResponse.swift */; }; 38899E7E2E794B420030F7CA /* SignUpResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E7C2E794B3D0030F7CA /* SignUpResponse.swift */; }; 38899E832E794C360030F7CA /* LoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E822E794C330030F7CA /* LoginResponse.swift */; }; @@ -420,8 +430,8 @@ 38899E972E7953310030F7CA /* NotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E952E7953300030F7CA /* NotificationInfoResponse.swift */; }; 38899E992E7954680030F7CA /* BlockedNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E982E7954670030F7CA /* BlockedNotificationInfoResponse.swift */; }; 38899E9A2E7954680030F7CA /* BlockedNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E982E7954670030F7CA /* BlockedNotificationInfoResponse.swift */; }; - 38899E9C2E7954D90030F7CA /* DeleteNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E9B2E7954D70030F7CA /* DeleteNotificationInfoResponse.swift */; }; - 38899E9D2E7954D90030F7CA /* DeleteNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E9B2E7954D70030F7CA /* DeleteNotificationInfoResponse.swift */; }; + 38899E9C2E7954D90030F7CA /* DeletedNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E9B2E7954D70030F7CA /* DeletedNotificationInfoResponse.swift */; }; + 38899E9D2E7954D90030F7CA /* DeletedNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899E9B2E7954D70030F7CA /* DeletedNotificationInfoResponse.swift */; }; 38899EA32E799B260030F7CA /* AppVersionRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899EA22E799B190030F7CA /* AppVersionRemoteDataSource.swift */; }; 38899EA42E799B260030F7CA /* AppVersionRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899EA22E799B190030F7CA /* AppVersionRemoteDataSource.swift */; }; 38899EA62E799BD60030F7CA /* AppVersionRemoteDataSourceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38899EA52E799BD10030F7CA /* AppVersionRemoteDataSourceImpl.swift */; }; @@ -550,16 +560,14 @@ 38B65E7D2E72ADB900DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B65E7B2E72ADB500DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift */; }; 38B6AACD2CA410D800CE6DB6 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AACC2CA410D800CE6DB6 /* MainTabBarController.swift */; }; 38B6AACE2CA410D800CE6DB6 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AACC2CA410D800CE6DB6 /* MainTabBarController.swift */; }; - 38B6AAD82CA424AE00CE6DB6 /* MoveTopButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AAD72CA424AE00CE6DB6 /* MoveTopButtonView.swift */; }; - 38B6AAD92CA424AE00CE6DB6 /* MoveTopButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AAD72CA424AE00CE6DB6 /* MoveTopButtonView.swift */; }; 38B6AADB2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AADA2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift */; }; 38B6AADC2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AADA2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift */; }; 38B6AADF2CA4777200CE6DB6 /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AADE2CA4777200CE6DB6 /* UIViewController+Rx.swift */; }; 38B6AAE02CA4777200CE6DB6 /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AADE2CA4777200CE6DB6 /* UIViewController+Rx.swift */; }; 38B6AAE22CA4787200CE6DB6 /* MainTabBarReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AAE12CA4787200CE6DB6 /* MainTabBarReactor.swift */; }; 38B6AAE32CA4787200CE6DB6 /* MainTabBarReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B6AAE12CA4787200CE6DB6 /* MainTabBarReactor.swift */; }; - 38B8A5842CAE9CC4000AFE83 /* MainHomeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A5832CAE9CC4000AFE83 /* MainHomeViewCell.swift */; }; - 38B8A5852CAE9CC4000AFE83 /* MainHomeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A5832CAE9CC4000AFE83 /* MainHomeViewCell.swift */; }; + 38B8A5842CAE9CC4000AFE83 /* HomeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A5832CAE9CC4000AFE83 /* HomeViewCell.swift */; }; + 38B8A5852CAE9CC4000AFE83 /* HomeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A5832CAE9CC4000AFE83 /* HomeViewCell.swift */; }; 38B8A5882CAEA5F9000AFE83 /* DetailViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A5872CAEA5F9000AFE83 /* DetailViewCell.swift */; }; 38B8A5892CAEA5F9000AFE83 /* DetailViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A5872CAEA5F9000AFE83 /* DetailViewCell.swift */; }; 38B8A58B2CAEA79A000AFE83 /* DetailViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B8A58A2CAEA79A000AFE83 /* DetailViewFooter.swift */; }; @@ -590,6 +598,16 @@ 38CE94C02C904D460004B238 /* SOMNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38CE94BE2C904D460004B238 /* SOMNavigationBar.swift */; }; 38D055C32CD862FE00E75590 /* SOMActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D055C22CD862FE00E75590 /* SOMActivityIndicatorView.swift */; }; 38D055C42CD862FE00E75590 /* SOMActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D055C22CD862FE00E75590 /* SOMActivityIndicatorView.swift */; }; + 38D2FBC12E812354006DD739 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBC02E81234C006DD739 /* HomeViewController.swift */; }; + 38D2FBC22E812354006DD739 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBC02E81234C006DD739 /* HomeViewController.swift */; }; + 38D2FBC42E81AD26006DD739 /* refrech_control_lottie.json in Resources */ = {isa = PBXBuildFile; fileRef = 38D2FBC32E81AD26006DD739 /* refrech_control_lottie.json */; }; + 38D2FBC52E81AD26006DD739 /* refrech_control_lottie.json in Resources */ = {isa = PBXBuildFile; fileRef = 38D2FBC32E81AD26006DD739 /* refrech_control_lottie.json */; }; + 38D2FBCB2E81B0E5006DD739 /* SOMSwipableTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBCA2E81B0DE006DD739 /* SOMSwipableTabBarItem.swift */; }; + 38D2FBCC2E81B0E5006DD739 /* SOMSwipableTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBCA2E81B0DE006DD739 /* SOMSwipableTabBarItem.swift */; }; + 38D2FBCE2E81B52F006DD739 /* SOMSwipableTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D2FBCD2E81B529006DD739 /* SOMSwipableTabBar.swift */; }; + 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 */; }; 38D3CB162CC2362B001EC280 /* Hakgyoansim-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 38D3CB142CC2362B001EC280 /* Hakgyoansim-Bold.ttf */; }; 38D3CB172CC2362B001EC280 /* Hakgyoansim-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 38D3CB142CC2362B001EC280 /* Hakgyoansim-Bold.ttf */; }; 38D3CB182CC2362B001EC280 /* Hakgyoansim-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 38D3CB152CC2362B001EC280 /* Hakgyoansim-Light.ttf */; }; @@ -598,12 +616,12 @@ 38D488CB2D0C557300F2D38D /* SOMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D488C92D0C557300F2D38D /* SOMButton.swift */; }; 38D522682E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D522672E742F550044911B /* SOMLoadingIndicatorView.swift */; }; 38D522692E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D522672E742F550044911B /* SOMLoadingIndicatorView.swift */; }; - 38D5637B2D16D72D006265AA /* SOMSwipeTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637A2D16D72D006265AA /* SOMSwipeTabBar.swift */; }; - 38D5637C2D16D72D006265AA /* SOMSwipeTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637A2D16D72D006265AA /* SOMSwipeTabBar.swift */; }; - 38D5637E2D17152F006265AA /* SOMSwipeTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637D2D17152F006265AA /* SOMSwipeTabBarDelegate.swift */; }; - 38D5637F2D17152F006265AA /* SOMSwipeTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637D2D17152F006265AA /* SOMSwipeTabBarDelegate.swift */; }; - 38D563842D1719B1006265AA /* SOMSwipeTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D563832D1719B1006265AA /* SOMSwipeTabBarItem.swift */; }; - 38D563852D1719B1006265AA /* SOMSwipeTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D563832D1719B1006265AA /* SOMSwipeTabBarItem.swift */; }; + 38D5637B2D16D72D006265AA /* SOMStickyTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637A2D16D72D006265AA /* SOMStickyTabBar.swift */; }; + 38D5637C2D16D72D006265AA /* SOMStickyTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637A2D16D72D006265AA /* SOMStickyTabBar.swift */; }; + 38D5637E2D17152F006265AA /* SOMStickyTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637D2D17152F006265AA /* SOMStickyTabBarDelegate.swift */; }; + 38D5637F2D17152F006265AA /* SOMStickyTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5637D2D17152F006265AA /* SOMStickyTabBarDelegate.swift */; }; + 38D563842D1719B1006265AA /* SOMStickyTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D563832D1719B1006265AA /* SOMStickyTabBarItem.swift */; }; + 38D563852D1719B1006265AA /* SOMStickyTabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D563832D1719B1006265AA /* SOMStickyTabBarItem.swift */; }; 38D5CE0B2CBCE8CA0054AB9A /* SimpleDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5CE0A2CBCE8CA0054AB9A /* SimpleDefaults.swift */; }; 38D5CE0C2CBCE8CA0054AB9A /* SimpleDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D5CE0A2CBCE8CA0054AB9A /* SimpleDefaults.swift */; }; 38D6F17C2CC2406700E11530 /* WriteCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D6F17B2CC2406700E11530 /* WriteCardViewController.swift */; }; @@ -636,18 +654,6 @@ 38F131892CC7B7E0000D0475 /* RelatedTagResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F131872CC7B7E0000D0475 /* RelatedTagResponse.swift */; }; 38F3D9302D06C2370049F575 /* SOMAnimationTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3D92F2D06C2370049F575 /* SOMAnimationTransitioning.swift */; }; 38F3D9312D06C2370049F575 /* SOMAnimationTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3D92F2D06C2370049F575 /* SOMAnimationTransitioning.swift */; }; - 38F70E5B2D1905D000B33C9D /* MainHomeTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E5A2D1905D000B33C9D /* MainHomeTabBarController.swift */; }; - 38F70E5C2D1905D000B33C9D /* MainHomeTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E5A2D1905D000B33C9D /* MainHomeTabBarController.swift */; }; - 38F70E5E2D190FBD00B33C9D /* MainHomeTabBarReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E5D2D190FBD00B33C9D /* MainHomeTabBarReactor.swift */; }; - 38F70E5F2D190FBD00B33C9D /* MainHomeTabBarReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E5D2D190FBD00B33C9D /* MainHomeTabBarReactor.swift */; }; - 38F70E622D19113E00B33C9D /* MainHomeLatestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E612D19113E00B33C9D /* MainHomeLatestViewController.swift */; }; - 38F70E632D19113E00B33C9D /* MainHomeLatestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E612D19113E00B33C9D /* MainHomeLatestViewController.swift */; }; - 38F70E652D19161800B33C9D /* MainHomeLatestViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E642D19161800B33C9D /* MainHomeLatestViewReactor.swift */; }; - 38F70E662D19161800B33C9D /* MainHomeLatestViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E642D19161800B33C9D /* MainHomeLatestViewReactor.swift */; }; - 38F70E6C2D191D9A00B33C9D /* MainHomePopularViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E6B2D191D9A00B33C9D /* MainHomePopularViewController.swift */; }; - 38F70E6D2D191D9A00B33C9D /* MainHomePopularViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E6B2D191D9A00B33C9D /* MainHomePopularViewController.swift */; }; - 38F70E6F2D191DFB00B33C9D /* MainHomePopularViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E6E2D191DFB00B33C9D /* MainHomePopularViewReactor.swift */; }; - 38F70E702D191DFB00B33C9D /* MainHomePopularViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F70E6E2D191DFB00B33C9D /* MainHomePopularViewReactor.swift */; }; 38F720A52CD4F15900DF32B5 /* CardSummaryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F7209A2CD4F15900DF32B5 /* CardSummaryResponse.swift */; }; 38F720A62CD4F15900DF32B5 /* CardSummaryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F7209A2CD4F15900DF32B5 /* CardSummaryResponse.swift */; }; 38F720A72CD4F15900DF32B5 /* CommentCardResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F7209B2CD4F15900DF32B5 /* CommentCardResponse.swift */; }; @@ -678,6 +684,16 @@ 38FDC2B72C9E746B00C094C2 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FDC2B52C9E746B00C094C2 /* BaseViewController.swift */; }; 38FDC2C72C9E764300C094C2 /* BaseNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FDC2C62C9E764300C094C2 /* BaseNavigationViewController.swift */; }; 38FDC2C82C9E764300C094C2 /* BaseNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FDC2C62C9E764300C094C2 /* BaseNavigationViewController.swift */; }; + 38FEBE542E865121002916A8 /* FollowNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE532E865119002916A8 /* FollowNotificationInfoResponse.swift */; }; + 38FEBE552E865121002916A8 /* FollowNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE532E865119002916A8 /* FollowNotificationInfoResponse.swift */; }; + 38FEBE5B2E8652DE002916A8 /* CompositeNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE5A2E8652D2002916A8 /* CompositeNotificationInfoResponse.swift */; }; + 38FEBE5C2E8652DE002916A8 /* CompositeNotificationInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE5A2E8652D2002916A8 /* CompositeNotificationInfoResponse.swift */; }; + 38FEBE5E2E86612C002916A8 /* NoticeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE5D2E866125002916A8 /* NoticeViewCell.swift */; }; + 38FEBE5F2E86612C002916A8 /* NoticeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE5D2E866125002916A8 /* NoticeViewCell.swift */; }; + 38FEBE612E8661F4002916A8 /* NoticeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE602E8661F2002916A8 /* NoticeInfo.swift */; }; + 38FEBE622E8661F4002916A8 /* NoticeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE602E8661F2002916A8 /* NoticeInfo.swift */; }; + 38FEBE642E8662A3002916A8 /* NoticeInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE632E86629F002916A8 /* NoticeInfoResponse.swift */; }; + 38FEBE652E8662A3002916A8 /* NoticeInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEBE632E86629F002916A8 /* NoticeInfoResponse.swift */; }; 9F83B2E5C38D60FE4D409059 /* Pods_SOOUM_DevTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D0E55D6E2C99714CF229CA1 /* Pods_SOOUM_DevTests.framework */; }; A40D6E37CF713608CA27DF02 /* Pods_SOOUM_Dev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 365609D6E03DDA36B6BDBE3A /* Pods_SOOUM_Dev.framework */; }; /* End PBXBuildFile section */ @@ -695,8 +711,6 @@ /* Begin PBXFileReference section */ 294DA89A179AE3234F3E293F /* Pods-SOOUM-Dev.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SOOUM-Dev.release.xcconfig"; path = "Target Support Files/Pods-SOOUM-Dev/Pods-SOOUM-Dev.release.xcconfig"; sourceTree = ""; }; 2A032EFC2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTermsOfServiceViewReactor.swift; sourceTree = ""; }; - 2A048E7A2C9BDF5F00FFD485 /* SOMLocationFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMLocationFilter.swift; sourceTree = ""; }; - 2A048E832C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMLocationFilterCollectionViewCell.swift; sourceTree = ""; }; 2A34AFB42D144EEF007BD7E7 /* EmptyTagDetailTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTagDetailTableViewCell.swift; sourceTree = ""; }; 2A44A4292CAC09AE00DC463E /* RSAKeyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSAKeyResponse.swift; sourceTree = ""; }; 2A44A42C2CAC14C800DC463E /* SignInResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInResponse.swift; sourceTree = ""; }; @@ -778,6 +792,15 @@ 3803CF812D017DB800FD90DB /* EnterMemberTransferViewController.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = EnterMemberTransferViewController.swift; sourceTree = ""; tabWidth = 4; }; 3803CF842D017DC700FD90DB /* EnterMemberTransferViewReactor.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = EnterMemberTransferViewReactor.swift; sourceTree = ""; tabWidth = 4; }; 3803CF872D01914200FD90DB /* ResignViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignViewReactor.swift; sourceTree = ""; }; + 380F42202E87ECA2009AC59E /* CompositeNotificationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeNotificationInfo.swift; sourceTree = ""; }; + 380F42232E884ADF009AC59E /* CardRemoteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRemoteDataSource.swift; sourceTree = ""; }; + 380F42262E884B6F009AC59E /* BaseCardInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCardInfo.swift; sourceTree = ""; }; + 380F42292E884E85009AC59E /* HomeCardInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCardInfoResponse.swift; sourceTree = ""; }; + 380F422C2E884F35009AC59E /* CardRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRemoteDataSourceImpl.swift; sourceTree = ""; }; + 380F422F2E884FB5009AC59E /* CardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepository.swift; sourceTree = ""; }; + 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepositoryImpl.swift; sourceTree = ""; }; + 380F42352E88502F009AC59E /* CardUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardUseCase.swift; sourceTree = ""; }; + 380F42382E885057009AC59E /* CardUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardUseCaseImpl.swift; sourceTree = ""; }; 38121E282CA6A52400602499 /* UIRefreshControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRefreshControl.swift; sourceTree = ""; }; 38121E302CA6C77500602499 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 38121E332CA6DA4000602499 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; @@ -794,10 +817,8 @@ 381A1D762CC3DA99005FDB8E /* SOMTagsLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMTagsLayout.swift; sourceTree = ""; }; 381DEA8A2CD4BBCB009F1FE9 /* WriteCardTextView+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteCardTextView+Rx.swift"; sourceTree = ""; }; 382D5CF52CFE9B8600BFA23E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; - 382E15352D15A6460097B09C /* NotificationTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTabBarController.swift; sourceTree = ""; }; 382E15392D15A67A0097B09C /* NotificationViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewCell.swift; sourceTree = ""; }; 382E153E2D15AF4F0097B09C /* CommentHistoryInNotiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentHistoryInNotiResponse.swift; sourceTree = ""; }; - 382E15412D15BA490097B09C /* NotificationWithReportViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationWithReportViewCell.swift; sourceTree = ""; }; 3830FFA52CEC6E3100ABA9FD /* Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Kingfisher.swift; sourceTree = ""; }; 3834FADC2D11C5AC00C9108D /* SimpleCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCache.swift; sourceTree = ""; }; 3836ACB32C8F045300A3C566 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = ""; }; @@ -826,16 +847,20 @@ 385053572C92DD2300C80B02 /* SOMTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMTabBarController.swift; sourceTree = ""; }; 385071992CA295A800A7905A /* LaunchScreenViewController.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewController.swift; sourceTree = ""; tabWidth = 4; }; 3854419A2C870544004E2BB0 /* SOOUM-Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SOOUM-Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 385602B52D2FB18400118530 /* NotiPlaceholderViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotiPlaceholderViewCell.swift; sourceTree = ""; }; + 385602B52D2FB18400118530 /* NotificationPlaceholderViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPlaceholderViewCell.swift; sourceTree = ""; }; 385620EE2CA19C9500E0AB5A /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 385620F12CA19D2D00E0AB5A /* Alamofire_Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alamofire_Request.swift; sourceTree = ""; }; 385620F52CA19EA900E0AB5A /* Alamofire_constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alamofire_constants.swift; sourceTree = ""; }; 38572CD72D2230C900B07C69 /* NotificationAllowResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAllowResponse.swift; sourceTree = ""; }; 38572CDA2D22464F00B07C69 /* PungTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PungTimeView.swift; sourceTree = ""; }; - 38572CDD2D2254E800B07C69 /* PlaceholderViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderViewCell.swift; sourceTree = ""; }; + 38572CDD2D2254E800B07C69 /* HomePlaceholderViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePlaceholderViewCell.swift; sourceTree = ""; }; 3857BC312D4D1A7B008D4264 /* SOOUM-DevTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SOOUM-DevTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 3857BC3E2D4D1FFA008D4264 /* MockManagerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManagerProvider.swift; sourceTree = ""; }; 3857BC402D4D22B4008D4264 /* MockManagerProviderContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockManagerProviderContainerTests.swift; sourceTree = ""; }; + 385C01AD2E8E8C6A003C7894 /* SOMPageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMPageModel.swift; sourceTree = ""; }; + 385C01B02E8E8DD4003C7894 /* SOMPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMPageView.swift; sourceTree = ""; }; + 385C01B32E8EA1B1003C7894 /* SOMPageViewsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMPageViewsDelegate.swift; sourceTree = ""; }; + 385C01B62E8EA1EB003C7894 /* SOMPageViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMPageViews.swift; sourceTree = ""; }; 385E65A22CBE56D00032E120 /* Coordinate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinate.swift; sourceTree = ""; }; 38608B2F2CB5195D0066BB40 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; 3862C0DE2C9EB6670023C046 /* UIViewController+PushAndPop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+PushAndPop.swift"; sourceTree = ""; }; @@ -867,7 +892,7 @@ 3878F4732CA3F06C00AA46A2 /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = ""; }; 3878F4762CA3F08300AA46A2 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 3878FE0C2D0365C800D8955C /* SOMNavigationBar+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SOMNavigationBar+Rx.swift"; sourceTree = ""; }; - 387D852B2D08320A005D9D22 /* SOMCardModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOMCardModel.swift; sourceTree = ""; }; + 387FA11C2E88DDBD004DF7CE /* HomeViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewReactor.swift; sourceTree = ""; }; 387FBAEC2C8702C100A5E139 /* SOOUM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SOOUM.app; sourceTree = BUILT_PRODUCTS_DIR; }; 387FBAEF2C8702C100A5E139 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 387FBAF12C8702C100A5E139 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -885,12 +910,9 @@ 388371FB2C8C8F11004212EB /* UIColor+SOOUM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+SOOUM.swift"; sourceTree = ""; }; 388372002C8C8FCF004212EB /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; 3886939E2CF77FA7005F9EF3 /* UIApplication+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Top.swift"; sourceTree = ""; }; - 388698502D191F2100008600 /* MainHomeDistanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeDistanceViewController.swift; sourceTree = ""; }; - 388698532D191F4B00008600 /* MainHomeDistanceViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeDistanceViewReactor.swift; sourceTree = ""; }; 388698572D1982DE00008600 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; 3886985E2D1984D600008600 /* NotificationViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewReactor.swift; sourceTree = ""; }; 388698612D1986B100008600 /* NotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequest.swift; sourceTree = ""; }; - 388698642D1998DB00008600 /* NotificationTabBarReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTabBarReactor.swift; sourceTree = ""; }; 388717692E7BD7AC00C6143B /* loading_indicator_lottie.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = loading_indicator_lottie.json; sourceTree = ""; }; 3887176C2E7BDBA800C6143B /* NicknameResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameResponse.swift; sourceTree = ""; }; 3887D0322CC5335200FB52E1 /* WriteCardViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardViewReactor.swift; sourceTree = ""; }; @@ -902,7 +924,6 @@ 38899E6A2E793AF70030F7CA /* CheckAvailable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckAvailable.swift; sourceTree = ""; }; 38899E6D2E79400B0030F7CA /* ImageUrlInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUrlInfoResponse.swift; sourceTree = ""; }; 38899E702E7940280030F7CA /* ImageUrlInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUrlInfo.swift; sourceTree = ""; }; - 38899E732E7943070030F7CA /* SignUpRequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRequestInfo.swift; sourceTree = ""; }; 38899E7C2E794B3D0030F7CA /* SignUpResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpResponse.swift; sourceTree = ""; }; 38899E822E794C330030F7CA /* LoginResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginResponse.swift; sourceTree = ""; }; 38899E852E794CE90030F7CA /* NetworkManager_FCM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager_FCM.swift; sourceTree = ""; }; @@ -912,7 +933,7 @@ 38899E922E79518E0030F7CA /* CommonNotificationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonNotificationInfo.swift; sourceTree = ""; }; 38899E952E7953300030F7CA /* NotificationInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationInfoResponse.swift; sourceTree = ""; }; 38899E982E7954670030F7CA /* BlockedNotificationInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedNotificationInfoResponse.swift; sourceTree = ""; }; - 38899E9B2E7954D70030F7CA /* DeleteNotificationInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNotificationInfoResponse.swift; sourceTree = ""; }; + 38899E9B2E7954D70030F7CA /* DeletedNotificationInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedNotificationInfoResponse.swift; sourceTree = ""; }; 38899EA22E799B190030F7CA /* AppVersionRemoteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionRemoteDataSource.swift; sourceTree = ""; }; 38899EA52E799BD10030F7CA /* AppVersionRemoteDataSourceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionRemoteDataSourceImpl.swift; sourceTree = ""; }; 38899EA82E799C5D0030F7CA /* VersionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionRequest.swift; sourceTree = ""; }; @@ -979,11 +1000,10 @@ 38B65E782E72A29100DF6919 /* OnboardingNumberingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNumberingView.swift; sourceTree = ""; }; 38B65E7B2E72ADB500DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TermsOfServiceAgreeButtonView+Rx.swift"; sourceTree = ""; }; 38B6AACC2CA410D800CE6DB6 /* MainTabBarController.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; tabWidth = 4; }; - 38B6AAD72CA424AE00CE6DB6 /* MoveTopButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveTopButtonView.swift; sourceTree = ""; }; 38B6AADA2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewReactor.swift; sourceTree = ""; tabWidth = 4; }; 38B6AADE2CA4777200CE6DB6 /* UIViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; 38B6AAE12CA4787200CE6DB6 /* MainTabBarReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarReactor.swift; sourceTree = ""; }; - 38B8A5832CAE9CC4000AFE83 /* MainHomeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeViewCell.swift; sourceTree = ""; }; + 38B8A5832CAE9CC4000AFE83 /* HomeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewCell.swift; sourceTree = ""; }; 38B8A5872CAEA5F9000AFE83 /* DetailViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewCell.swift; sourceTree = ""; }; 38B8A58A2CAEA79A000AFE83 /* DetailViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewFooter.swift; sourceTree = ""; }; 38B8A58D2CAEB61A000AFE83 /* DetailViewFooterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewFooterCell.swift; sourceTree = ""; }; @@ -1001,13 +1021,18 @@ 38CC49872CDE3972007A0145 /* SOMPresentationController+Show.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SOMPresentationController+Show.swift"; sourceTree = ""; }; 38CE94BE2C904D460004B238 /* SOMNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMNavigationBar.swift; sourceTree = ""; }; 38D055C22CD862FE00E75590 /* SOMActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMActivityIndicatorView.swift; sourceTree = ""; }; + 38D2FBC02E81234C006DD739 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + 38D2FBC32E81AD26006DD739 /* refrech_control_lottie.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = refrech_control_lottie.json; sourceTree = ""; }; + 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 = ""; }; 38D3CB142CC2362B001EC280 /* Hakgyoansim-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Hakgyoansim-Bold.ttf"; sourceTree = ""; }; 38D3CB152CC2362B001EC280 /* Hakgyoansim-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Hakgyoansim-Light.ttf"; 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 /* SOMSwipeTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMSwipeTabBar.swift; sourceTree = ""; }; - 38D5637D2D17152F006265AA /* SOMSwipeTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMSwipeTabBarDelegate.swift; sourceTree = ""; }; - 38D563832D1719B1006265AA /* SOMSwipeTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMSwipeTabBarItem.swift; sourceTree = ""; }; + 38D5637A2D16D72D006265AA /* SOMStickyTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMStickyTabBar.swift; sourceTree = ""; }; + 38D5637D2D17152F006265AA /* SOMStickyTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMStickyTabBarDelegate.swift; sourceTree = ""; }; + 38D563832D1719B1006265AA /* SOMStickyTabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMStickyTabBarItem.swift; sourceTree = ""; }; 38D5CE0A2CBCE8CA0054AB9A /* SimpleDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleDefaults.swift; sourceTree = ""; }; 38D6F17B2CC2406700E11530 /* WriteCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardViewController.swift; sourceTree = ""; }; 38D6F17F2CC2413400E11530 /* WriteCardTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCardTextView.swift; sourceTree = ""; }; @@ -1025,12 +1050,6 @@ 38F006A92D395A7F001AC5F7 /* SuspensionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspensionResponse.swift; sourceTree = ""; }; 38F131872CC7B7E0000D0475 /* RelatedTagResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedTagResponse.swift; sourceTree = ""; }; 38F3D92F2D06C2370049F575 /* SOMAnimationTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOMAnimationTransitioning.swift; sourceTree = ""; }; - 38F70E5A2D1905D000B33C9D /* MainHomeTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeTabBarController.swift; sourceTree = ""; }; - 38F70E5D2D190FBD00B33C9D /* MainHomeTabBarReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeTabBarReactor.swift; sourceTree = ""; }; - 38F70E612D19113E00B33C9D /* MainHomeLatestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeLatestViewController.swift; sourceTree = ""; }; - 38F70E642D19161800B33C9D /* MainHomeLatestViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeLatestViewReactor.swift; sourceTree = ""; }; - 38F70E6B2D191D9A00B33C9D /* MainHomePopularViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomePopularViewController.swift; sourceTree = ""; }; - 38F70E6E2D191DFB00B33C9D /* MainHomePopularViewReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomePopularViewReactor.swift; sourceTree = ""; }; 38F7209A2CD4F15900DF32B5 /* CardSummaryResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSummaryResponse.swift; sourceTree = ""; }; 38F7209B2CD4F15900DF32B5 /* CommentCardResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentCardResponse.swift; sourceTree = ""; }; 38F7209E2CD4F15900DF32B5 /* DetailCardResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailCardResponse.swift; sourceTree = ""; }; @@ -1046,6 +1065,11 @@ 38FD4DB32D034F6600BF5FF1 /* MyFollowerViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyFollowerViewCell.swift; sourceTree = ""; }; 38FDC2B52C9E746B00C094C2 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; 38FDC2C62C9E764300C094C2 /* BaseNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationViewController.swift; sourceTree = ""; }; + 38FEBE532E865119002916A8 /* FollowNotificationInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowNotificationInfoResponse.swift; sourceTree = ""; }; + 38FEBE5A2E8652D2002916A8 /* CompositeNotificationInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeNotificationInfoResponse.swift; sourceTree = ""; }; + 38FEBE5D2E866125002916A8 /* NoticeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeViewCell.swift; sourceTree = ""; }; + 38FEBE602E8661F2002916A8 /* NoticeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeInfo.swift; sourceTree = ""; }; + 38FEBE632E86629F002916A8 /* NoticeInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeInfoResponse.swift; sourceTree = ""; }; 4C597C004C07775E636659FE /* Pods-SOOUM.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SOOUM.release.xcconfig"; path = "Target Support Files/Pods-SOOUM/Pods-SOOUM.release.xcconfig"; sourceTree = ""; }; 74194BC62F22BC2F5596D850 /* Pods_SOOUM.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SOOUM.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79FB23C15AD70915D59D7DC3 /* Pods-SOOUM.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SOOUM.debug.xcconfig"; path = "Target Support Files/Pods-SOOUM/Pods-SOOUM.debug.xcconfig"; sourceTree = ""; }; @@ -1083,15 +1107,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2A048E792C9BDF1000FFD485 /* SOMLocationFilter */ = { - isa = PBXGroup; - children = ( - 2A048E7A2C9BDF5F00FFD485 /* SOMLocationFilter.swift */, - 2A048E832C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift */, - ); - path = SOMLocationFilter; - sourceTree = ""; - }; 2A34AFB32D144EE7007BD7E7 /* Cells */ = { isa = PBXGroup; children = ( @@ -1542,9 +1557,9 @@ 382E15382D15A6680097B09C /* cells */ = { isa = PBXGroup; children = ( + 38FEBE5D2E866125002916A8 /* NoticeViewCell.swift */, 382E15392D15A67A0097B09C /* NotificationViewCell.swift */, - 382E15412D15BA490097B09C /* NotificationWithReportViewCell.swift */, - 385602B52D2FB18400118530 /* NotiPlaceholderViewCell.swift */, + 385602B52D2FB18400118530 /* NotificationPlaceholderViewCell.swift */, ); path = cells; sourceTree = ""; @@ -1860,15 +1875,6 @@ path = CommentHistory; sourceTree = ""; }; - 3878F46F2CA3F01100AA46A2 /* SOMCard */ = { - isa = PBXGroup; - children = ( - 387D852B2D08320A005D9D22 /* SOMCardModel.swift */, - 3878F4702CA3F03400AA46A2 /* SOMCard.swift */, - ); - path = SOMCard; - sourceTree = ""; - }; 3878FE0B2D0365B000D8955C /* SOMNavigationBar */ = { isa = PBXGroup; children = ( @@ -1929,6 +1935,7 @@ 387FBB052C87038F00A5E139 /* Resources */ = { isa = PBXGroup; children = ( + 38D2FBC32E81AD26006DD739 /* refrech_control_lottie.json */, 388717692E7BD7AC00C6143B /* loading_indicator_lottie.json */, 387FBAF82C8702C200A5E139 /* Assets.xcassets */, 2A6280602D085C6200803BE9 /* SOOUM.entitlements */, @@ -2072,15 +2079,6 @@ path = Auth; sourceTree = ""; }; - 388698562D19829400008600 /* Views */ = { - isa = PBXGroup; - children = ( - 388698572D1982DE00008600 /* NotificationViewController.swift */, - 3886985E2D1984D600008600 /* NotificationViewReactor.swift */, - ); - path = Views; - sourceTree = ""; - }; 38899E562E7936CD0030F7CA /* Style */ = { isa = PBXGroup; children = ( @@ -2093,6 +2091,7 @@ 38899E5A2E79374E0030F7CA /* Repositories */ = { isa = PBXGroup; children = ( + 380F42322E884FD4009AC59E /* CardRepositoryImpl.swift */, 3889A28E2E79D8800030F7CA /* NotificationRepositoryImpl.swift */, 3889A2822E79D7CE0030F7CA /* AuthRepositoryImpl.swift */, 3889A2612E79BB540030F7CA /* UserRepositoryImpl.swift */, @@ -2124,11 +2123,15 @@ 38899E632E7938CD0030F7CA /* Responses */ = { isa = PBXGroup; children = ( + 380F42292E884E85009AC59E /* HomeCardInfoResponse.swift */, + 38FEBE632E86629F002916A8 /* NoticeInfoResponse.swift */, 3887176C2E7BDBA800C6143B /* NicknameResponse.swift */, 3889A27C2E79C5670030F7CA /* ToeknResponse.swift */, - 38899E9B2E7954D70030F7CA /* DeleteNotificationInfoResponse.swift */, - 38899E982E7954670030F7CA /* BlockedNotificationInfoResponse.swift */, + 38FEBE5A2E8652D2002916A8 /* CompositeNotificationInfoResponse.swift */, 38899E952E7953300030F7CA /* NotificationInfoResponse.swift */, + 38FEBE532E865119002916A8 /* FollowNotificationInfoResponse.swift */, + 38899E9B2E7954D70030F7CA /* DeletedNotificationInfoResponse.swift */, + 38899E982E7954670030F7CA /* BlockedNotificationInfoResponse.swift */, 38899E8E2E79511F0030F7CA /* KeyInfoResponse.swift */, 38899E8B2E794E680030F7CA /* AppVersionStatusResponse.swift */, 38899E822E794C330030F7CA /* LoginResponse.swift */, @@ -2153,10 +2156,12 @@ 38899E692E793AEA0030F7CA /* Models */ = { isa = PBXGroup; children = ( + 380F42262E884B6F009AC59E /* BaseCardInfo.swift */, + 380F42202E87ECA2009AC59E /* CompositeNotificationInfo.swift */, + 38FEBE602E8661F2002916A8 /* NoticeInfo.swift */, 3889A27F2E79D0230030F7CA /* Token.swift */, 38899E922E79518E0030F7CA /* CommonNotificationInfo.swift */, 38F88EBD2D2C1E22002AD7A8 /* Version.swift */, - 38899E732E7943070030F7CA /* SignUpRequestInfo.swift */, 38899E702E7940280030F7CA /* ImageUrlInfo.swift */, 38899E6A2E793AF70030F7CA /* CheckAvailable.swift */, ); @@ -2175,6 +2180,7 @@ 38899E9F2E799A7E0030F7CA /* Remotes */ = { isa = PBXGroup; children = ( + 380F422C2E884F35009AC59E /* CardRemoteDataSourceImpl.swift */, 3889A2762E79C2980030F7CA /* NotificationRemoteDataSoruceImpl.swift */, 3889A26D2E79BE970030F7CA /* AuthRemoteDataSourceImpl.swift */, 3889A2552E79BA0F0030F7CA /* UserRemoteDataSourceImpl.swift */, @@ -2187,6 +2193,7 @@ 38899EA02E799AA30030F7CA /* Interfaces */ = { isa = PBXGroup; children = ( + 380F42232E884ADF009AC59E /* CardRemoteDataSource.swift */, 3889A2732E79C1D30030F7CA /* NotificationRemoteDataSource.swift */, 3889A26A2E79BD410030F7CA /* AuthRemoteDataSource.swift */, 3889A24F2E79B3210030F7CA /* UserRemoteDataSource.swift */, @@ -2199,6 +2206,7 @@ isa = PBXGroup; children = ( 38389B9B2CCCF98B006728AF /* AuthRequest.swift */, + 384972A02CA4DEC00012FCA1 /* CardRequest.swift */, 388698612D1986B100008600 /* NotificationRequest.swift */, 38899EAC2E79A0990030F7CA /* UserRequest.swift */, 38899EA82E799C5D0030F7CA /* VersionRequest.swift */, @@ -2209,6 +2217,7 @@ 3889A2402E79AD320030F7CA /* UseCases */ = { isa = PBXGroup; children = ( + 380F42382E885057009AC59E /* CardUseCaseImpl.swift */, 3889A2942E79D9200030F7CA /* NotificationUseCaseImpl.swift */, 3889A2882E79D81E0030F7CA /* AuthUseCaseImpl.swift */, 3889A2672E79BC840030F7CA /* UserUseCaseImpl.swift */, @@ -2221,6 +2230,7 @@ 3889A2412E79AD3A0030F7CA /* Repositories */ = { isa = PBXGroup; children = ( + 380F422F2E884FB5009AC59E /* CardRepository.swift */, 3889A28B2E79D8650030F7CA /* NotificationRepository.swift */, 3889A2702E79C0370030F7CA /* AuthRepository.swift */, 3889A25B2E79BB2F0030F7CA /* UserRepository.swift */, @@ -2232,6 +2242,7 @@ 3889A2482E79AE870030F7CA /* Interfaces */ = { isa = PBXGroup; children = ( + 380F42352E88502F009AC59E /* CardUseCase.swift */, 3889A2912E79D8F40030F7CA /* NotificationUseCase.swift */, 3889A2852E79D8060030F7CA /* AuthUseCase.swift */, 3889A2642E79BBC00030F7CA /* UserUseCase.swift */, @@ -2252,10 +2263,9 @@ 389596A12D15A4CB000662B6 /* Notification */ = { isa = PBXGroup; children = ( - 382E15352D15A6460097B09C /* NotificationTabBarController.swift */, - 388698642D1998DB00008600 /* NotificationTabBarReactor.swift */, + 388698572D1982DE00008600 /* NotificationViewController.swift */, + 3886985E2D1984D600008600 /* NotificationViewReactor.swift */, 382E15382D15A6680097B09C /* cells */, - 388698562D19829400008600 /* Views */, ); path = Notification; sourceTree = ""; @@ -2347,27 +2357,15 @@ 38B6AACF2CA411DE00CE6DB6 /* Home */ = { isa = PBXGroup; children = ( - 38F70E5A2D1905D000B33C9D /* MainHomeTabBarController.swift */, - 38F70E5D2D190FBD00B33C9D /* MainHomeTabBarReactor.swift */, + 38D2FBC02E81234C006DD739 /* HomeViewController.swift */, + 387FA11C2E88DDBD004DF7CE /* HomeViewReactor.swift */, 38B8A5822CAE9CB7000AFE83 /* Cells */, - 38B6AAD32CA41B2C00CE6DB6 /* Views */, - 38F70E602D19112A00B33C9D /* Latest */, - 38F70E6A2D191D7D00B33C9D /* Popular */, - 38F70E712D191EE000B33C9D /* Distance */, 388009792CABEE18002A9209 /* Detail */, 389596A12D15A4CB000662B6 /* Notification */, ); path = Home; sourceTree = ""; }; - 38B6AAD32CA41B2C00CE6DB6 /* Views */ = { - isa = PBXGroup; - children = ( - 38B6AAD72CA424AE00CE6DB6 /* MoveTopButtonView.swift */, - ); - path = Views; - sourceTree = ""; - }; 38B6AADD2CA4775500CE6DB6 /* RxCocoa */ = { isa = PBXGroup; children = ( @@ -2406,7 +2404,6 @@ children = ( 38899EAB2E799E360030F7CA /* V2 */, 2A5ABA332D464E0B00BF6C9B /* ConfigureRequest.swift */, - 384972A02CA4DEC00012FCA1 /* CardRequest.swift */, 2AE6B1592CBEAEC000FA5C3C /* ReportRequest.swift */, 2A5BB7E22CDCD97300E1C799 /* JoinRequest.swift */, 3878D04D2CFFC5F300F9522F /* ProfileRequest.swift */, @@ -2419,8 +2416,8 @@ 38B8A5822CAE9CB7000AFE83 /* Cells */ = { isa = PBXGroup; children = ( - 38B8A5832CAE9CC4000AFE83 /* MainHomeViewCell.swift */, - 38572CDD2D2254E800B07C69 /* PlaceholderViewCell.swift */, + 38B8A5832CAE9CC4000AFE83 /* HomeViewCell.swift */, + 38572CDD2D2254E800B07C69 /* HomePlaceholderViewCell.swift */, ); path = Cells; sourceTree = ""; @@ -2471,12 +2468,13 @@ 38AE56572D0489E500CAA431 /* SOMDialogController */, 38CC49802CDE382C007A0145 /* SOMPresentationController */, 3880098C2CABF4AD002A9209 /* SOMTags */, - 3878F46F2CA3F01100AA46A2 /* SOMCard */, - 2A048E792C9BDF1000FFD485 /* SOMLocationFilter */, + 38E5AA822E8D28DB005D676B /* SOMPageViews */, 385053502C92DBCA00C80B02 /* SOMTabBarController */, - 38D563792D16D58B006265AA /* SOMSwipeTabBar */, + 38D2FBC92E81B0B2006DD739 /* SOMSwipableTabBar */, + 38D563792D16D58B006265AA /* SOMStickyTabBar */, 3878FE0B2D0365B000D8955C /* SOMNavigationBar */, 38D488C92D0C557300F2D38D /* SOMButton.swift */, + 3878F4702CA3F03400AA46A2 /* SOMCard.swift */, 38773E7B2CB3ACB2004815CD /* SOMRefreshControl.swift */, 38D055C22CD862FE00E75590 /* SOMActivityIndicatorView.swift */, 38D522672E742F550044911B /* SOMLoadingIndicatorView.swift */, @@ -2495,14 +2493,24 @@ name = Frameworks; sourceTree = ""; }; - 38D563792D16D58B006265AA /* SOMSwipeTabBar */ = { + 38D2FBC92E81B0B2006DD739 /* SOMSwipableTabBar */ = { + isa = PBXGroup; + children = ( + 38D2FBD02E81B9B0006DD739 /* SOMSwipableTabBarDelegate.swift */, + 38D2FBCD2E81B529006DD739 /* SOMSwipableTabBar.swift */, + 38D2FBCA2E81B0DE006DD739 /* SOMSwipableTabBarItem.swift */, + ); + path = SOMSwipableTabBar; + sourceTree = ""; + }; + 38D563792D16D58B006265AA /* SOMStickyTabBar */ = { isa = PBXGroup; children = ( - 38D5637D2D17152F006265AA /* SOMSwipeTabBarDelegate.swift */, - 38D563832D1719B1006265AA /* SOMSwipeTabBarItem.swift */, - 38D5637A2D16D72D006265AA /* SOMSwipeTabBar.swift */, + 38D5637D2D17152F006265AA /* SOMStickyTabBarDelegate.swift */, + 38D5637A2D16D72D006265AA /* SOMStickyTabBar.swift */, + 38D563832D1719B1006265AA /* SOMStickyTabBarItem.swift */, ); - path = SOMSwipeTabBar; + path = SOMStickyTabBar; sourceTree = ""; }; 38D5CE0D2CBCF7210054AB9A /* Card */ = { @@ -2566,40 +2574,24 @@ path = Push; sourceTree = ""; }; - 38F131862CC7B7AB000D0475 /* WriteCard */ = { + 38E5AA822E8D28DB005D676B /* SOMPageViews */ = { isa = PBXGroup; children = ( - 38F131872CC7B7E0000D0475 /* RelatedTagResponse.swift */, - 2ACBD4152CC9631B0057C013 /* UploadCard */, + 385C01B32E8EA1B1003C7894 /* SOMPageViewsDelegate.swift */, + 385C01AD2E8E8C6A003C7894 /* SOMPageModel.swift */, + 385C01B02E8E8DD4003C7894 /* SOMPageView.swift */, + 385C01B62E8EA1EB003C7894 /* SOMPageViews.swift */, ); - path = WriteCard; + path = SOMPageViews; sourceTree = ""; }; - 38F70E602D19112A00B33C9D /* Latest */ = { - isa = PBXGroup; - children = ( - 38F70E612D19113E00B33C9D /* MainHomeLatestViewController.swift */, - 38F70E642D19161800B33C9D /* MainHomeLatestViewReactor.swift */, - ); - path = Latest; - sourceTree = ""; - }; - 38F70E6A2D191D7D00B33C9D /* Popular */ = { - isa = PBXGroup; - children = ( - 38F70E6B2D191D9A00B33C9D /* MainHomePopularViewController.swift */, - 38F70E6E2D191DFB00B33C9D /* MainHomePopularViewReactor.swift */, - ); - path = Popular; - sourceTree = ""; - }; - 38F70E712D191EE000B33C9D /* Distance */ = { + 38F131862CC7B7AB000D0475 /* WriteCard */ = { isa = PBXGroup; children = ( - 388698502D191F2100008600 /* MainHomeDistanceViewController.swift */, - 388698532D191F4B00008600 /* MainHomeDistanceViewReactor.swift */, + 38F131872CC7B7E0000D0475 /* RelatedTagResponse.swift */, + 2ACBD4152CC9631B0057C013 /* UploadCard */, ); - path = Distance; + path = WriteCard; sourceTree = ""; }; 38F720A02CD4F15900DF32B5 /* Detail */ = { @@ -2751,6 +2743,7 @@ files = ( 385441952C870544004E2BB0 /* Assets.xcassets in Resources */, 38D3CB172CC2362B001EC280 /* Hakgyoansim-Bold.ttf in Resources */, + 38D2FBC42E81AD26006DD739 /* refrech_control_lottie.json in Resources */, 385441962C870544004E2BB0 /* Base in Resources */, 38D3CB192CC2362B001EC280 /* Hakgyoansim-Light.ttf in Resources */, 2A62805B2D084FEB00803BE9 /* GoogleService-Info.plist in Resources */, @@ -2772,6 +2765,7 @@ files = ( 387894462D31788800F69487 /* GoogleService-Info.plist in Resources */, 387FBAF92C8702C200A5E139 /* Assets.xcassets in Resources */, + 38D2FBC52E81AD26006DD739 /* refrech_control_lottie.json in Resources */, 38D3CB162CC2362B001EC280 /* Hakgyoansim-Bold.ttf in Resources */, 387FBAFC2C8702C200A5E139 /* Base in Resources */, 38D3CB182CC2362B001EC280 /* Hakgyoansim-Light.ttf in Resources */, @@ -2952,7 +2946,6 @@ 381DEA8E2CD4BE55009F1FE9 /* SignInResponse.swift in Sources */, 381A1D662CC38E7D005FDB8E /* WriteTagTextField.swift in Sources */, 3802BDB92D0AF2F7001256EA /* PushManager+Rx.swift in Sources */, - 387D852D2D08320A005D9D22 /* SOMCardModel.swift in Sources */, 385620F72CA19EA900E0AB5A /* Alamofire_constants.swift in Sources */, 3817016F2CD882C2005FC220 /* TimeoutInterceptor.swift in Sources */, 38AE565D2D048B4800CAA431 /* SOMDialogViewController+Show.swift in Sources */, @@ -2966,11 +2959,11 @@ 2AE6B1672CBFB81000FA5C3C /* UploadCardBottomSheetViewReactor.swift in Sources */, 3836ACB52C8F045300A3C566 /* Typography.swift in Sources */, 38389BA02CCCFB7D006728AF /* AuthKeyChain.swift in Sources */, - 2A048E852C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift in Sources */, 2A5ABA342D464E0B00BF6C9B /* ConfigureRequest.swift in Sources */, 38899E902E7951200030F7CA /* KeyInfoResponse.swift in Sources */, 388371FA2C8C8EB1004212EB /* SooumStyle.swift in Sources */, 38F720B42CD4F15900DF32B5 /* LatestCardResponse.swift in Sources */, + 385C01B12E8E8DD8003C7894 /* SOMPageView.swift in Sources */, 3803CF6A2D0156BA00FD90DB /* SettingsViewReactor.swift in Sources */, 3878F4782CA3F08300AA46A2 /* UIView.swift in Sources */, 2ACBD4182CC963390057C013 /* DefaultCardImageResponse.swift in Sources */, @@ -2980,6 +2973,7 @@ 2AFD054A2CFF7687007C84AD /* RecommendTagsResponse.swift in Sources */, 38773E7D2CB3ACB2004815CD /* SOMRefreshControl.swift in Sources */, 3889A2572E79BA160030F7CA /* UserRemoteDataSourceImpl.swift in Sources */, + 385C01B42E8EA1B7003C7894 /* SOMPageViewsDelegate.swift in Sources */, 3889A24D2E79AEB30030F7CA /* AppVersionUseCaseImpl.swift in Sources */, 38CC49862CDE3885007A0145 /* SOMTransitioningDelegate.swift in Sources */, 388DA0FC2C8F521300A9DD56 /* FontContainer.swift in Sources */, @@ -3001,31 +2995,32 @@ 3816C05D2CCDDF3D00C8688C /* ReAuthenticationResponse.swift in Sources */, 38FDC2B72C9E746B00C094C2 /* BaseViewController.swift in Sources */, 38CC49892CDE3972007A0145 /* SOMPresentationController+Show.swift in Sources */, - 38F70E5C2D1905D000B33C9D /* MainHomeTabBarController.swift in Sources */, 38E7FBF02D3CF6BC00A359CD /* SOMDialogAction.swift in Sources */, 3878D07E2CFFE6E500F9522F /* IssueMemberTransferViewController.swift in Sources */, 3878B8632D0DC8BD00B3B128 /* UIViewController+Toast.swift in Sources */, - 38899E742E79430A0030F7CA /* SignUpRequestInfo.swift in Sources */, 38E9CE112D376E0E00E85A2D /* PushTokenSet.swift in Sources */, - 38F70E702D191DFB00B33C9D /* MainHomePopularViewReactor.swift in Sources */, 3878D0982CFFF2B800F9522F /* CommentHistroyViewController.swift in Sources */, 38899E6F2E79400C0030F7CA /* ImageUrlInfoResponse.swift in Sources */, 38F720AE2CD4F15900DF32B5 /* DetailCardResponse.swift in Sources */, 3889A2772E79C29F0030F7CA /* NotificationRemoteDataSoruceImpl.swift in Sources */, 2AFD056A2D03264C007C84AD /* AddFavoriteTagResponse.swift in Sources */, + 38FEBE552E865121002916A8 /* FollowNotificationInfoResponse.swift in Sources */, 2AFD05532D007F2F007C84AD /* TagSearchViewReactor.swift in Sources */, - 38899E9C2E7954D90030F7CA /* DeleteNotificationInfoResponse.swift in Sources */, + 38899E9C2E7954D90030F7CA /* DeletedNotificationInfoResponse.swift in Sources */, 2A44A42B2CAC09AE00DC463E /* RSAKeyResponse.swift in Sources */, 38899E8C2E794E690030F7CA /* AppVersionStatusResponse.swift in Sources */, 38A721962E73E7140071E1D8 /* View+SwiftEntryKit.swift in Sources */, 38572CD92D2230C900B07C69 /* NotificationAllowResponse.swift in Sources */, + 380F422B2E884E9C009AC59E /* HomeCardInfoResponse.swift in Sources */, 382E15402D15AF4F0097B09C /* CommentHistoryInNotiResponse.swift in Sources */, 388698632D1986B100008600 /* NotificationRequest.swift in Sources */, 2A980BA52D803EEA007DFA45 /* SOMEvent.swift in Sources */, 38B65E7C2E72ADB900DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift in Sources */, 38C2D4152CFEA9CC00CEA092 /* MyProfileViewCell.swift in Sources */, 38F720B22CD4F15900DF32B5 /* distanceCardResponse.swift in Sources */, + 38D2FBCE2E81B52F006DD739 /* SOMSwipableTabBar.swift in Sources */, 38FD4DB52D034F6600BF5FF1 /* MyFollowerViewCell.swift in Sources */, + 380F42372E885032009AC59E /* CardUseCase.swift in Sources */, 382E153B2D15A67A0097B09C /* NotificationViewCell.swift in Sources */, 388009922CABF855002A9209 /* SOMTagModel.swift in Sources */, 2AFD05502CFF79D8007C84AD /* TagsViewReactor.swift in Sources */, @@ -3036,12 +3031,15 @@ 2A5BB7E82CDCDC3600E1C799 /* NicknameValidationResponse.swift in Sources */, 383EC6122E7A4F6B00EC2D1E /* AuthLocalDataSource.swift in Sources */, 2AFF95622CF33A3900CBFB12 /* FavoriteTagView.swift in Sources */, + 385C01B82E8EA1EF003C7894 /* SOMPageViews.swift in Sources */, + 385C01AE2E8E8C6F003C7894 /* SOMPageModel.swift in Sources */, 2A032EFE2CE517DD008326C0 /* OnboardingTermsOfServiceViewReactor.swift in Sources */, 388C96372CCE41700061C598 /* AuthInfo.swift in Sources */, 3878D0892CFFEF0F00F9522F /* TermsOfServiceTextCellView+Rx.swift in Sources */, 38F720B62CD4F15900DF32B5 /* PopularCardResponse.swift in Sources */, 3878D0702CFFDF9600F9522F /* SettingTextCellView.swift in Sources */, 2A5BB7BF2CDB870000E1C799 /* OnboardingGuideMessageView.swift in Sources */, + 38FEBE612E8661F4002916A8 /* NoticeInfo.swift in Sources */, 388A2D2E2D00A45800E2F2F0 /* writtenCardResponse.swift in Sources */, 3878D07A2CFFE1E800F9522F /* ResignViewController.swift in Sources */, 38899EAD2E79A09B0030F7CA /* UserRequest.swift in Sources */, @@ -3054,7 +3052,9 @@ 3889A2872E79D8090030F7CA /* AuthUseCase.swift in Sources */, 2A5BB7CE2CDBB7D100E1C799 /* OnboardingProfileImageSettingViewController.swift in Sources */, 3893B6D22D36739500F2004C /* CompositeManager.swift in Sources */, + 380F42302E884FBC009AC59E /* CardRepository.swift in Sources */, 3800575D2D9C12CB00E58A19 /* DefinedError.swift in Sources */, + 387FA11E2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */, 38899E942E79518F0030F7CA /* CommonNotificationInfo.swift in Sources */, 38F131892CC7B7E0000D0475 /* RelatedTagResponse.swift in Sources */, 2AFD056E2D048CAF007C84AD /* TagRequest.swift in Sources */, @@ -3064,7 +3064,7 @@ 38816D9F2D004A5E00EB87D6 /* UpdateProfileViewController.swift in Sources */, 38D6F1842CC243DB00E11530 /* UITextView+Typography.swift in Sources */, 2AFF95792CF5F0B000CBFB12 /* TagPreviewCardView.swift in Sources */, - 38572CDF2D2254E800B07C69 /* PlaceholderViewCell.swift in Sources */, + 38572CDF2D2254E800B07C69 /* HomePlaceholderViewCell.swift in Sources */, 38B6AACE2CA410D800CE6DB6 /* MainTabBarController.swift in Sources */, 2AE6B1512CBCC2F600FA5C3C /* ReportTableViewCell.swift in Sources */, 388371FD2C8C8F11004212EB /* UIColor+SOOUM.swift in Sources */, @@ -3085,7 +3085,6 @@ 383EC6232E7A56CE00EC2D1E /* AppDIContainer.swift in Sources */, 382D5CF72CFE9B8600BFA23E /* ProfileViewController.swift in Sources */, 38899E892E794D620030F7CA /* NetworkManager_Version.swift in Sources */, - 388698552D191F4B00008600 /* MainHomeDistanceViewReactor.swift in Sources */, 385620F32CA19D2D00E0AB5A /* Alamofire_Request.swift in Sources */, 388A2D312D00D6A100E2F2F0 /* FollowViewReactor.swift in Sources */, 2A649ED42CAE990B002D8284 /* SOMDialogViewController.swift in Sources */, @@ -3096,6 +3095,7 @@ 385441912C870544004E2BB0 /* AppDelegate.swift in Sources */, 38D522692E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */, 38D869642CF821F900BF87DA /* UserDefaults.swift in Sources */, + 380F422D2E884F3D009AC59E /* CardRemoteDataSourceImpl.swift in Sources */, 38899E672E7939600030F7CA /* CheckAvailableResponse.swift in Sources */, 2AE6B1902CC121BB00FA5C3C /* BottomSheetSegmentTableViewCell.swift in Sources */, 38AE77DB2E745FFF00B6FD13 /* EnterMemberTransferTextFieldView.swift in Sources */, @@ -3104,34 +3104,36 @@ 381701722CD88374005FC220 /* CompositeInterceptor.swift in Sources */, 2A5BB7CA2CDBA53E00E1C799 /* OnboardingNicknameSettingViewController.swift in Sources */, 385620F02CA19C9500E0AB5A /* NetworkManager.swift in Sources */, + 380F42282E884B80009AC59E /* BaseCardInfo.swift in Sources */, 3889A24B2E79AE960030F7CA /* AppVersionUseCase.swift in Sources */, 2A980BA12D803EB1007DFA45 /* AnalyticsEventProtocol.swift in Sources */, 3803CF712D0159A500FD90DB /* SettingsResponse.swift in Sources */, 2A5BB7E12CDCBE7E00E1C799 /* OnboardingNicknameSettingViewReactor.swift in Sources */, 3803CF7E2D016DA200FD90DB /* TransferCodeResponse.swift in Sources */, - 38F70E5F2D190FBD00B33C9D /* MainHomeTabBarReactor.swift in Sources */, 3834FADE2D11C5AC00C9108D /* SimpleCache.swift in Sources */, 388009982CAC20EC002A9209 /* SOMTags+Rx.swift in Sources */, 38899EAA2E799C630030F7CA /* VersionRequest.swift in Sources */, + 38FEBE5B2E8652DE002916A8 /* CompositeNotificationInfoResponse.swift in Sources */, 2A980BA92D803F04007DFA45 /* GAManager.swift in Sources */, 2AFF95752CF5F08700CBFB12 /* TagPreviewCardCollectionViewCell.swift in Sources */, + 38FEBE642E8662A3002916A8 /* NoticeInfoResponse.swift in Sources */, 38899E722E79402C0030F7CA /* ImageUrlInfo.swift in Sources */, 2AFD05472CFF75DD007C84AD /* FavoriteTagsResponse.swift in Sources */, 3889A25C2E79BB340030F7CA /* UserRepository.swift in Sources */, - 38B6AAD92CA424AE00CE6DB6 /* MoveTopButtonView.swift in Sources */, 38899E7E2E794B420030F7CA /* SignUpResponse.swift in Sources */, 2AE6B16E2CBFBC7600FA5C3C /* UploadCardBottomSheetSegmentView.swift in Sources */, 38AA00032CAD1BCC002C5F1E /* LikeAndCommentView.swift in Sources */, + 38D2FBD12E81B9B7006DD739 /* SOMSwipableTabBarDelegate.swift in Sources */, + 38D2FBC12E812354006DD739 /* HomeViewController.swift in Sources */, 3816C0612CCDE35300C8688C /* ErrorInterceptor.swift in Sources */, 3803CF7B2D016BDB00FD90DB /* IssueMemberTransferViewReactor.swift in Sources */, - 38D5637C2D16D72D006265AA /* SOMSwipeTabBar.swift in Sources */, + 38D5637C2D16D72D006265AA /* SOMStickyTabBar.swift in Sources */, 3878D04F2CFFC5F300F9522F /* ProfileRequest.swift in Sources */, 38FDC2C82C9E764300C094C2 /* BaseNavigationViewController.swift in Sources */, 3880097C2CABEE3D002A9209 /* DetailViewController.swift in Sources */, 2AE6B1762CBFD59B00FA5C3C /* ImageCollectionViewCell.swift in Sources */, 2AE6B1642CBFB7FB00FA5C3C /* UploadCardBottomSheetViewController.swift in Sources */, 2AE6B1722CBFD04900FA5C3C /* SelectDefaultImageTableViewCell.swift in Sources */, - 382E15432D15BA490097B09C /* NotificationWithReportViewCell.swift in Sources */, 2A45B3702CE4C5510071026A /* RegisterUserResponse.swift in Sources */, 3889A2802E79D0250030F7CA /* Token.swift in Sources */, 388DA0FF2C8F526C00A9DD56 /* UIFont.swift in Sources */, @@ -3151,17 +3153,18 @@ 3889A2892E79D8220030F7CA /* AuthUseCaseImpl.swift in Sources */, 3880098F2CABF4C2002A9209 /* SOMTag.swift in Sources */, 3803CF892D01914200FD90DB /* ResignViewReactor.swift in Sources */, - 38F70E632D19113E00B33C9D /* MainHomeLatestViewController.swift in Sources */, 388A2D342D00D7BF00E2F2F0 /* UpdateProfileViewReactor.swift in Sources */, 38B543E62D4617CB00DDF2C5 /* PushManagerConfiguration.swift in Sources */, 3889A2462E79ADCE0030F7CA /* AppVersionRepositoryImpl.swift in Sources */, 2AE6B14A2CBC15BF00FA5C3C /* ReportViewController.swift in Sources */, 388D8AE02E73E6190044BA79 /* SwiftEntryKit.swift in Sources */, 38899E962E7953310030F7CA /* NotificationInfoResponse.swift in Sources */, + 38FEBE5F2E86612C002916A8 /* NoticeViewCell.swift in Sources */, 2AFD055A2D008D23007C84AD /* TagDetailViewController.swift in Sources */, 2AE6B1552CBCC34B00FA5C3C /* ReportReasonView.swift in Sources */, 2AFF95562CF3222400CBFB12 /* TagsViewController.swift in Sources */, 38121E322CA6C77500602499 /* Double.swift in Sources */, + 380F42252E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */, 38405CCC2CC611FD00612D1E /* BaseEmptyAndHeader.swift in Sources */, 38E9CE1A2D37FED000E85A2D /* AddingTokenInterceptor.swift in Sources */, 38F88EBB2D2C1CB8002AD7A8 /* Info.swift in Sources */, @@ -3175,10 +3178,9 @@ 2AFD056D2D048C84007C84AD /* FavoriteTagTableViewCell.swift in Sources */, 2A5BB7BA2CDB860D00E1C799 /* OnboardingTermsOfServiceViewController.swift in Sources */, 385441922C870544004E2BB0 /* SceneDelegate.swift in Sources */, - 38D563852D1719B1006265AA /* SOMSwipeTabBarItem.swift in Sources */, + 38D563852D1719B1006265AA /* SOMStickyTabBarItem.swift in Sources */, 389EF8182D2F450000E053AE /* Log.swift in Sources */, 3889A26B2E79BD450030F7CA /* AuthRemoteDataSource.swift in Sources */, - 382E15372D15A6460097B09C /* NotificationTabBarController.swift in Sources */, 38F720A82CD4F15900DF32B5 /* CommentCardResponse.swift in Sources */, 38C2D41B2CFEAAED00CEA092 /* ProfileViewFooter.swift in Sources */, 389681112CAFBD6A00FFD89F /* DetailViewReactor.swift in Sources */, @@ -3194,20 +3196,17 @@ 38C2D4182CFEAACA00CEA092 /* ProfileViewFooterCell.swift in Sources */, 381DEA8C2CD4BBCB009F1FE9 /* WriteCardTextView+Rx.swift in Sources */, 3889A2752E79C1D80030F7CA /* NotificationRemoteDataSource.swift in Sources */, - 388698662D1998DB00008600 /* NotificationTabBarReactor.swift in Sources */, 381A1D752CC3D799005FDB8E /* SOMTagsDelegate.swift in Sources */, - 2A048E7C2C9BDF5F00FFD485 /* SOMLocationFilter.swift in Sources */, 3803CF832D017DB800FD90DB /* EnterMemberTransferViewController.swift in Sources */, 38F720A62CD4F15900DF32B5 /* CardSummaryResponse.swift in Sources */, 388693A02CF77FA7005F9EF3 /* UIApplication+Top.swift in Sources */, 38D6F1872CC24C4F00E11530 /* WriteCardTextViewDelegate.swift in Sources */, 38B8BE482D1ECBDA0084569C /* NotificationInfo.swift in Sources */, - 38D5637F2D17152F006265AA /* SOMSwipeTabBarDelegate.swift in Sources */, + 38D5637F2D17152F006265AA /* SOMStickyTabBarDelegate.swift in Sources */, 388372022C8C8FCF004212EB /* UIColor.swift in Sources */, 38B543E92D4617EA00DDF2C5 /* NetworkManagerConfiguration.swift in Sources */, 388698592D1982DE00008600 /* NotificationViewController.swift in Sources */, 38389B9D2CCCF98B006728AF /* AuthRequest.swift in Sources */, - 38F70E6D2D191D9A00B33C9D /* MainHomePopularViewController.swift in Sources */, 385E65A42CBE56D00032E120 /* Coordinate.swift in Sources */, 388009952CABFAAA002A9209 /* SOMTags.swift in Sources */, 3878F4752CA3F06C00AA46A2 /* UIStackView.swift in Sources */, @@ -3225,13 +3224,14 @@ 2AE6B1792CBFE49D00FA5C3C /* SelectFontTableViewCell.swift in Sources */, 389EF81B2D2F454600E053AE /* Log+Extract.swift in Sources */, 381701792CD88854005FC220 /* LogginMonitor.swift in Sources */, - 388698522D191F2100008600 /* MainHomeDistanceViewController.swift in Sources */, 388DA1052C8F545E00A9DD56 /* Typography+SOOUM.swift in Sources */, + 38D2FBCC2E81B0E5006DD739 /* SOMSwipableTabBarItem.swift in Sources */, 2AFD05612D009FA1007C84AD /* TagDetailViewrReactor.swift in Sources */, 3887D0372CC5335D00FB52E1 /* WriteCardView.swift in Sources */, 3889A2952E79D9250030F7CA /* NotificationUseCaseImpl.swift in Sources */, 38B8A58C2CAEA79A000AFE83 /* DetailViewFooter.swift in Sources */, 3889A2512E79B3260030F7CA /* UserRemoteDataSource.swift in Sources */, + 380F42222E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */, 38899E5E2E7937E50030F7CA /* NicknameValidateResponse.swift in Sources */, 385053532C92DBE200C80B02 /* SOMTabBarItem.swift in Sources */, 3803CF6D2D0156FC00FD90DB /* SettingsRequest.swift in Sources */, @@ -3243,7 +3243,7 @@ 2AFF95652CF33D9F00CBFB12 /* TagsHeaderView.swift in Sources */, 38B543EC2D461B1A00DDF2C5 /* LocationManagerConfigruation.swift in Sources */, 38A5D1552C8CB12300B68363 /* UIImage+SOOUM.swift in Sources */, - 385602B72D2FB18400118530 /* NotiPlaceholderViewCell.swift in Sources */, + 385602B72D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */, 383EC6152E7A50EB00EC2D1E /* AuthLocalDataSourceImpl.swift in Sources */, 3878D0542CFFC6C100F9522F /* ProfileResponse.swift in Sources */, 3889A27D2E79C56E0030F7CA /* ToeknResponse.swift in Sources */, @@ -3251,7 +3251,6 @@ 38FD4DAF2D032FCE00BF5FF1 /* AnnouncementViewReactor.swift in Sources */, 3802BDAD2D0AC1FB001256EA /* UIImage.swift in Sources */, 3889A2622E79BB5B0030F7CA /* UserRepositoryImpl.swift in Sources */, - 38F70E662D19161800B33C9D /* MainHomeLatestViewReactor.swift in Sources */, 3889A2842E79D7D40030F7CA /* AuthRepositoryImpl.swift in Sources */, 383EC6202E7A564600EC2D1E /* AppAssembler.swift in Sources */, 381DEA8D2CD4BE4A009F1FE9 /* Card.swift in Sources */, @@ -3262,8 +3261,10 @@ 3878F4722CA3F03400AA46A2 /* SOMCard.swift in Sources */, 3803CF862D017DC700FD90DB /* EnterMemberTransferViewReactor.swift in Sources */, 38B6AAE32CA4787200CE6DB6 /* MainTabBarReactor.swift in Sources */, + 380F42392E88505B009AC59E /* CardUseCaseImpl.swift in Sources */, 38899E592E7936DD0030F7CA /* SooumStyle_V2.swift in Sources */, - 38B8A5852CAE9CC4000AFE83 /* MainHomeViewCell.swift in Sources */, + 380F42342E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */, + 38B8A5852CAE9CC4000AFE83 /* HomeViewCell.swift in Sources */, 3878D0642CFFD66700F9522F /* FollowViewController.swift in Sources */, 38026E402CA2B45A0045E1CE /* LocationManager.swift in Sources */, 381A1D782CC3DA99005FDB8E /* SOMTagsLayout.swift in Sources */, @@ -3297,11 +3298,10 @@ 385620F62CA19EA900E0AB5A /* Alamofire_constants.swift in Sources */, 38F88EBA2D2C1CB8002AD7A8 /* Info.swift in Sources */, 3834FADD2D11C5AC00C9108D /* SimpleCache.swift in Sources */, - 38D563842D1719B1006265AA /* SOMSwipeTabBarItem.swift in Sources */, + 38D563842D1719B1006265AA /* SOMStickyTabBarItem.swift in Sources */, 2A5BB7BE2CDB870000E1C799 /* OnboardingGuideMessageView.swift in Sources */, 3836ACB42C8F045300A3C566 /* Typography.swift in Sources */, 2AFD05632D00A1E1007C84AD /* TagDetailCardResponse.swift in Sources */, - 2A048E842C9BE01300FFD485 /* SOMLocationFilterCollectionViewCell.swift in Sources */, 38AE565C2D048B4800CAA431 /* SOMDialogViewController+Show.swift in Sources */, 385E65A32CBE56D00032E120 /* Coordinate.swift in Sources */, 3889A2692E79BC880030F7CA /* UserUseCaseImpl.swift in Sources */, @@ -3318,7 +3318,7 @@ 38D6F17C2CC2406700E11530 /* WriteCardViewController.swift in Sources */, 38899E8F2E7951200030F7CA /* KeyInfoResponse.swift in Sources */, 3878F4772CA3F08300AA46A2 /* UIView.swift in Sources */, - 388698512D191F2100008600 /* MainHomeDistanceViewController.swift in Sources */, + 385C01B22E8E8DD8003C7894 /* SOMPageView.swift in Sources */, 3830FFA62CEC6E3100ABA9FD /* Kingfisher.swift in Sources */, 3878D0672CFFDAF100F9522F /* OtherFollowViewCell.swift in Sources */, 381701782CD88854005FC220 /* LogginMonitor.swift in Sources */, @@ -3328,6 +3328,7 @@ 2AE6B14C2CBC160C00FA5C3C /* ReportViewReactor.swift in Sources */, 388009912CABF855002A9209 /* SOMTagModel.swift in Sources */, 3889A2562E79BA160030F7CA /* UserRemoteDataSourceImpl.swift in Sources */, + 385C01B52E8EA1B7003C7894 /* SOMPageViewsDelegate.swift in Sources */, 3889A24E2E79AEB30030F7CA /* AppVersionUseCaseImpl.swift in Sources */, 38FD4DAB2D032CF000BF5FF1 /* AnnouncementResponse.swift in Sources */, 388FCAD02CFAC2BF0012C4D6 /* Notification.swift in Sources */, @@ -3339,9 +3340,8 @@ 388DA0FB2C8F521000A9DD56 /* FontContainer.swift in Sources */, 38572CD82D2230C900B07C69 /* NotificationAllowResponse.swift in Sources */, 38B543DF2D46171300DDF2C5 /* ManagerConfiguration.swift in Sources */, - 38D5637E2D17152F006265AA /* SOMSwipeTabBarDelegate.swift in Sources */, + 38D5637E2D17152F006265AA /* SOMStickyTabBarDelegate.swift in Sources */, 2AE6B17B2CBFE9ED00FA5C3C /* UploadCardSettingTableViewCell.swift in Sources */, - 38F70E5B2D1905D000B33C9D /* MainHomeTabBarController.swift in Sources */, 385009C22D363525007175A1 /* FilterNil.swift in Sources */, 38B6AADB2CA4740B00CE6DB6 /* LaunchScreenViewReactor.swift in Sources */, 383EC61C2E7A548E00EC2D1E /* BaseDIContainer.swift in Sources */, @@ -3353,27 +3353,28 @@ 38AA00022CAD1BCC002C5F1E /* LikeAndCommentView.swift in Sources */, 2AE6B1542CBCC34B00FA5C3C /* ReportReasonView.swift in Sources */, 38D5CE0B2CBCE8CA0054AB9A /* SimpleDefaults.swift in Sources */, - 38899E752E79430A0030F7CA /* SignUpRequestInfo.swift in Sources */, 38E9CE102D376E0E00E85A2D /* PushTokenSet.swift in Sources */, 2AFD05602D009FA1007C84AD /* TagDetailViewrReactor.swift in Sources */, 385053582C92DD2300C80B02 /* SOMTabBarController.swift in Sources */, 38899E6E2E79400C0030F7CA /* ImageUrlInfoResponse.swift in Sources */, 381A1D6A2CC398B3005FDB8E /* WriteTagTextFieldDelegate.swift in Sources */, 3889A2782E79C29F0030F7CA /* NotificationRemoteDataSoruceImpl.swift in Sources */, - 38F70E622D19113E00B33C9D /* MainHomeLatestViewController.swift in Sources */, - 38F70E6F2D191DFB00B33C9D /* MainHomePopularViewReactor.swift in Sources */, - 38899E9D2E7954D90030F7CA /* DeleteNotificationInfoResponse.swift in Sources */, + 38FEBE542E865121002916A8 /* FollowNotificationInfoResponse.swift in Sources */, + 38899E9D2E7954D90030F7CA /* DeletedNotificationInfoResponse.swift in Sources */, 3878D0792CFFE1E800F9522F /* ResignViewController.swift in Sources */, 38899E8D2E794E690030F7CA /* AppVersionStatusResponse.swift in Sources */, 38A721952E73E7140071E1D8 /* View+SwiftEntryKit.swift in Sources */, 38F720B12CD4F15900DF32B5 /* distanceCardResponse.swift in Sources */, + 380F422A2E884E9C009AC59E /* HomeCardInfoResponse.swift in Sources */, 38816DA22D004DED00EB87D6 /* UpdateProfileView.swift in Sources */, 38FDC2B62C9E746B00C094C2 /* BaseViewController.swift in Sources */, 2A980BA42D803EEA007DFA45 /* SOMEvent.swift in Sources */, 38B65E7D2E72ADB900DF6919 /* TermsOfServiceAgreeButtonView+Rx.swift in Sources */, 2AFD05492CFF7687007C84AD /* RecommendTagsResponse.swift in Sources */, 2AE6B17F2CBFEA5200FA5C3C /* ToggleView.swift in Sources */, + 38D2FBCF2E81B52F006DD739 /* SOMSwipableTabBar.swift in Sources */, 38601E1B2D3139D000A465A9 /* RecommendTagView.swift in Sources */, + 380F42362E885032009AC59E /* CardUseCase.swift in Sources */, 388009942CABFAAA002A9209 /* SOMTags.swift in Sources */, 3862C0DF2C9EB6670023C046 /* UIViewController+PushAndPop.swift in Sources */, 38F720B52CD4F15900DF32B5 /* PopularCardResponse.swift in Sources */, @@ -3385,11 +3386,14 @@ 383EC6112E7A4F6B00EC2D1E /* AuthLocalDataSource.swift in Sources */, 388A2D302D00D6A100E2F2F0 /* FollowViewReactor.swift in Sources */, 389EF81E2D2F469B00E053AE /* CocoaLumberjack.swift in Sources */, + 385C01B72E8EA1EF003C7894 /* SOMPageViews.swift in Sources */, + 385C01AF2E8E8C6F003C7894 /* SOMPageModel.swift in Sources */, 3880098E2CABF4C2002A9209 /* SOMTag.swift in Sources */, 2A5BB7E32CDCD97300E1C799 /* JoinRequest.swift in Sources */, 38C2D4202CFEB82400CEA092 /* ProfileViewReactor.swift in Sources */, 2A44A4372CAC227300DC463E /* BaseAuthResponse.swift in Sources */, 38CC49822CDE3854007A0145 /* SOMPresentationController.swift in Sources */, + 38FEBE622E8661F4002916A8 /* NoticeInfo.swift in Sources */, 3802BDAC2D0AC1FB001256EA /* UIImage.swift in Sources */, 389EF81A2D2F454600E053AE /* Log+Extract.swift in Sources */, 38899EAE2E79A09B0030F7CA /* UserRequest.swift in Sources */, @@ -3402,6 +3406,8 @@ 3889A2862E79D8090030F7CA /* AuthUseCase.swift in Sources */, 3886985F2D1984D600008600 /* NotificationViewReactor.swift in Sources */, 2AFD05692D03264C007C84AD /* AddFavoriteTagResponse.swift in Sources */, + 380F42312E884FBC009AC59E /* CardRepository.swift in Sources */, + 387FA11D2E88DDC1004DF7CE /* HomeViewReactor.swift in Sources */, 3800575C2D9C12CB00E58A19 /* DefinedError.swift in Sources */, 38899E932E79518F0030F7CA /* CommonNotificationInfo.swift in Sources */, 3893B6D12D36739500F2004C /* CompositeManager.swift in Sources */, @@ -3444,6 +3450,7 @@ 3880097B2CABEE3D002A9209 /* DetailViewController.swift in Sources */, 38D522682E742F610044911B /* SOMLoadingIndicatorView.swift in Sources */, 38D869632CF821F900BF87DA /* UserDefaults.swift in Sources */, + 380F422E2E884F3D009AC59E /* CardRemoteDataSourceImpl.swift in Sources */, 38899E662E7939600030F7CA /* CheckAvailableResponse.swift in Sources */, 385620F22CA19D2D00E0AB5A /* Alamofire_Request.swift in Sources */, 38AE77DC2E745FFF00B6FD13 /* EnterMemberTransferTextFieldView.swift in Sources */, @@ -3452,6 +3459,7 @@ 3878B8622D0DC8BD00B3B128 /* UIViewController+Toast.swift in Sources */, 2AFD05522D007F2F007C84AD /* TagSearchViewReactor.swift in Sources */, 387FBAF02C8702C100A5E139 /* AppDelegate.swift in Sources */, + 380F42272E884B80009AC59E /* BaseCardInfo.swift in Sources */, 3889A24A2E79AE960030F7CA /* AppVersionUseCase.swift in Sources */, 2A980BA02D803EB1007DFA45 /* AnalyticsEventProtocol.swift in Sources */, 38B8A58E2CAEB61A000AFE83 /* DetailViewFooterCell.swift in Sources */, @@ -3461,8 +3469,9 @@ 3887D0332CC5335200FB52E1 /* WriteCardViewReactor.swift in Sources */, 38D6F1802CC2413400E11530 /* WriteCardTextView.swift in Sources */, 38899EA92E799C630030F7CA /* VersionRequest.swift in Sources */, + 38FEBE5C2E8652DE002916A8 /* CompositeNotificationInfoResponse.swift in Sources */, 2A980BA82D803F04007DFA45 /* GAManager.swift in Sources */, - 387D852C2D08320A005D9D22 /* SOMCardModel.swift in Sources */, + 38FEBE652E8662A3002916A8 /* NoticeInfoResponse.swift in Sources */, 38899E712E79402C0030F7CA /* ImageUrlInfo.swift in Sources */, 2AFD055D2D009513007C84AD /* TagDetailNavigationBarView.swift in Sources */, 3889A25D2E79BB340030F7CA /* UserRepository.swift in Sources */, @@ -3470,14 +3479,14 @@ 38899E7D2E794B420030F7CA /* SignUpResponse.swift in Sources */, 3803CF692D0156BA00FD90DB /* SettingsViewReactor.swift in Sources */, 38F720B82CD4F16500DF32B5 /* CardProtocol.swift in Sources */, + 38D2FBD22E81B9B7006DD739 /* SOMSwipableTabBarDelegate.swift in Sources */, + 38D2FBC22E812354006DD739 /* HomeViewController.swift in Sources */, 38F131882CC7B7E0000D0475 /* RelatedTagResponse.swift in Sources */, 38405CCB2CC611FD00612D1E /* BaseEmptyAndHeader.swift in Sources */, 381701712CD88374005FC220 /* CompositeInterceptor.swift in Sources */, - 38B6AAD82CA424AE00CE6DB6 /* MoveTopButtonView.swift in Sources */, 38FDC2C72C9E764300C094C2 /* BaseNavigationViewController.swift in Sources */, 3803CF702D0159A500FD90DB /* SettingsResponse.swift in Sources */, - 38D5637B2D16D72D006265AA /* SOMSwipeTabBar.swift in Sources */, - 382E15422D15BA490097B09C /* NotificationWithReportViewCell.swift in Sources */, + 38D5637B2D16D72D006265AA /* SOMStickyTabBar.swift in Sources */, 2AE6B1712CBFD04900FA5C3C /* SelectDefaultImageTableViewCell.swift in Sources */, 2AE6B1662CBFB81000FA5C3C /* UploadCardBottomSheetViewReactor.swift in Sources */, 38FD4DB12D034C1700BF5FF1 /* MyFollowingViewCell.swift in Sources */, @@ -3497,7 +3506,6 @@ 2AFD05462CFF75DD007C84AD /* FavoriteTagsResponse.swift in Sources */, 2AFD054F2CFF79D8007C84AD /* TagsViewReactor.swift in Sources */, 3889A28A2E79D8220030F7CA /* AuthUseCaseImpl.swift in Sources */, - 388698652D1998DB00008600 /* NotificationTabBarReactor.swift in Sources */, 388DA0FE2C8F526C00A9DD56 /* UIFont.swift in Sources */, 38F720AD2CD4F15900DF32B5 /* DetailCardResponse.swift in Sources */, 38B543E52D4617CB00DDF2C5 /* PushManagerConfiguration.swift in Sources */, @@ -3506,14 +3514,15 @@ 388D8ADF2E73E6190044BA79 /* SwiftEntryKit.swift in Sources */, 38899E972E7953310030F7CA /* NotificationInfoResponse.swift in Sources */, 2AFF95552CF3222400CBFB12 /* TagsViewController.swift in Sources */, + 38FEBE5E2E86612C002916A8 /* NoticeViewCell.swift in Sources */, 2AE6B1922CC1286D00FA5C3C /* SelectMyImageTableViewCell.swift in Sources */, 2AE6B18F2CC121BB00FA5C3C /* BottomSheetSegmentTableViewCell.swift in Sources */, 38F3D9302D06C2370049F575 /* SOMAnimationTransitioning.swift in Sources */, - 385602B62D2FB18400118530 /* NotiPlaceholderViewCell.swift in Sources */, + 385602B62D2FB18400118530 /* NotificationPlaceholderViewCell.swift in Sources */, 38E9CE192D37FED000E85A2D /* AddingTokenInterceptor.swift in Sources */, + 380F42242E884AE5009AC59E /* CardRemoteDataSource.swift in Sources */, 3802BDB82D0AF2F7001256EA /* PushManager+Rx.swift in Sources */, 3878D05C2CFFD10D00F9522F /* FollowingResponse.swift in Sources */, - 38F70E652D19161800B33C9D /* MainHomeLatestViewReactor.swift in Sources */, 38E9CE132D37711600E85A2D /* OnboardingViewReactor.swift in Sources */, 3836ACBA2C8F050D00A3C566 /* UILabel+Typography.swift in Sources */, 38CC49852CDE3885007A0145 /* SOMTransitioningDelegate.swift in Sources */, @@ -3523,13 +3532,11 @@ 2A5BB7CD2CDBB7D100E1C799 /* OnboardingProfileImageSettingViewController.swift in Sources */, 385053552C92DCF900C80B02 /* SOMTabBar.swift in Sources */, 387FBAF22C8702C100A5E139 /* SceneDelegate.swift in Sources */, - 2A048E7B2C9BDF5F00FFD485 /* SOMLocationFilter.swift in Sources */, 38121E312CA6C77500602499 /* Double.swift in Sources */, 3889A26C2E79BD450030F7CA /* AuthRemoteDataSource.swift in Sources */, 3878D0532CFFC6C100F9522F /* ProfileResponse.swift in Sources */, 2AE6B1752CBFD59B00FA5C3C /* ImageCollectionViewCell.swift in Sources */, 38601E1A2D3139BB00A465A9 /* TagInfoResponse.swift in Sources */, - 382E15362D15A6460097B09C /* NotificationTabBarController.swift in Sources */, 3889A2922E79D8F80030F7CA /* NotificationUseCase.swift in Sources */, 2A34AFB52D144F08007BD7E7 /* EmptyTagDetailTableViewCell.swift in Sources */, 3803CF742D0166D700FD90DB /* CommentHistoryViewCell.swift in Sources */, @@ -3552,7 +3559,6 @@ 3878F4742CA3F06C00AA46A2 /* UIStackView.swift in Sources */, 2A5BB7D92CDCBA8400E1C799 /* OnboardingNicknameTextFieldView.swift in Sources */, 38B543E82D4617EA00DDF2C5 /* NetworkManagerConfiguration.swift in Sources */, - 38F70E6C2D191D9A00B33C9D /* MainHomePopularViewController.swift in Sources */, 3878D0902CFFF0E300F9522F /* AnnouncementViewCell.swift in Sources */, 2AFD054C2CFF76CB007C84AD /* TagRequest.swift in Sources */, 38B6AADF2CA4777200CE6DB6 /* UIViewController+Rx.swift in Sources */, @@ -3563,7 +3569,6 @@ 38899E842E794C360030F7CA /* LoginResponse.swift in Sources */, 385053522C92DBE200C80B02 /* SOMTabBarItem.swift in Sources */, 38A5D1542C8CB11E00B68363 /* UIImage+SOOUM.swift in Sources */, - 38F70E5E2D190FBD00B33C9D /* MainHomeTabBarReactor.swift in Sources */, 38601E182D31399400A465A9 /* CardRequest.swift in Sources */, 38899EA32E799B260030F7CA /* AppVersionRemoteDataSource.swift in Sources */, 2A5BB7FA2CE277AF00E1C799 /* OnboardingProfileImageSettingViewReactor.swift in Sources */, @@ -3571,15 +3576,17 @@ 38899EA72E799BD60030F7CA /* AppVersionRemoteDataSourceImpl.swift in Sources */, 38AE77D52E74580000B6FD13 /* OnboardingCompletedViewController.swift in Sources */, 3878D06F2CFFDF9600F9522F /* SettingTextCellView.swift in Sources */, - 38B8A5842CAE9CC4000AFE83 /* MainHomeViewCell.swift in Sources */, + 38B8A5842CAE9CC4000AFE83 /* HomeViewCell.swift in Sources */, 2AFF95612CF33A3900CBFB12 /* FavoriteTagView.swift in Sources */, 3803CF852D017DC700FD90DB /* EnterMemberTransferViewReactor.swift in Sources */, 38D488CA2D0C557300F2D38D /* SOMButton.swift in Sources */, + 38D2FBCB2E81B0E5006DD739 /* SOMSwipableTabBarItem.swift in Sources */, 3878F4712CA3F03400AA46A2 /* SOMCard.swift in Sources */, 3878D06B2CFFDF1F00F9522F /* SettingsViewController.swift in Sources */, 3889A2962E79D9250030F7CA /* NotificationUseCaseImpl.swift in Sources */, 3878D0882CFFEF0F00F9522F /* TermsOfServiceTextCellView+Rx.swift in Sources */, 3889A2502E79B3260030F7CA /* UserRemoteDataSource.swift in Sources */, + 380F42212E87ECA3009AC59E /* CompositeNotificationInfo.swift in Sources */, 38899E5F2E7937E50030F7CA /* NicknameValidateResponse.swift in Sources */, 2AE6B16D2CBFBC7600FA5C3C /* UploadCardBottomSheetSegmentView.swift in Sources */, 38F720A72CD4F15900DF32B5 /* CommentCardResponse.swift in Sources */, @@ -3609,10 +3616,11 @@ 38AE77DF2E7465F500B6FD13 /* EnterMemberTransferTextFieldView+Rx.swift in Sources */, 38738D4B2D2FDCC300C37574 /* WithoutReadNotisCountResponse.swift in Sources */, 388698622D1986B100008600 /* NotificationRequest.swift in Sources */, - 38572CDE2D2254E800B07C69 /* PlaceholderViewCell.swift in Sources */, + 38572CDE2D2254E800B07C69 /* HomePlaceholderViewCell.swift in Sources */, + 380F423A2E88505B009AC59E /* CardUseCaseImpl.swift in Sources */, 38899E582E7936DD0030F7CA /* SooumStyle_V2.swift in Sources */, + 380F42332E884FDC009AC59E /* CardRepositoryImpl.swift in Sources */, 388009972CAC20EC002A9209 /* SOMTags+Rx.swift in Sources */, - 388698542D191F4B00008600 /* MainHomeDistanceViewReactor.swift in Sources */, 38601E192D3139A500A465A9 /* TagDetailViewController.swift in Sources */, 3866577E2CEF3554009F7F60 /* UIButton+Rx.swift in Sources */, ); @@ -3650,7 +3658,7 @@ CODE_SIGN_ENTITLEMENTS = "SOOUM/Resources/SOOUM-Dev.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1013030; + CURRENT_PROJECT_VERSION = 1014040; DEVELOPMENT_TEAM = 99FRG743RX; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SOOUM/Resources/Develop/Info-dev.plist"; @@ -3673,7 +3681,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.13.3; + MARKETING_VERSION = 1.14.4; OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3685,6 +3693,7 @@ SOOUM_SERVER_ENDPOINT = "test-core.sooum.org:555"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -3701,7 +3710,7 @@ CODE_SIGN_ENTITLEMENTS = "SOOUM/Resources/SOOUM-Dev.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1013030; + CURRENT_PROJECT_VERSION = 1014040; DEVELOPMENT_TEAM = 99FRG743RX; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SOOUM/Resources/Develop/Info-dev.plist"; @@ -3724,7 +3733,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.13.3; + MARKETING_VERSION = 1.14.4; OTHER_SWIFT_FLAGS = "$(inherited) -D DEVELOP"; PRODUCT_BUNDLE_IDENTIFIER = com.sooum.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3736,6 +3745,7 @@ SOOUM_SERVER_ENDPOINT = "test-core.sooum.org:555"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/SOOUM/SOOUM/App/AppDelegate.swift b/SOOUM/SOOUM/App/AppDelegate.swift index 8c14591f..35266cd0 100644 --- a/SOOUM/SOOUM/App/AppDelegate.swift +++ b/SOOUM/SOOUM/App/AppDelegate.swift @@ -16,6 +16,7 @@ import FirebaseMessaging import RxSwift import CocoaLumberjack +import Kingfisher @main @@ -45,6 +46,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Initalize token self.initializeTokenWhenFirstLaunch() + // Set Kinfisher caching limit + self.setupKingfisherCacheLimit() + FirebaseApp.configure() // 파이어베이스 Meesaging 설정 Messaging.messaging().delegate = self @@ -188,6 +192,19 @@ extension AppDelegate { AuthKeyChain.shared.delete(.refreshToken) } + private func setupKingfisherCacheLimit() { + let cache = ImageCache.default + + /// 500MB + let diskLimit: UInt = 500 * 1024 * 1024 + cache.diskStorage.config.sizeLimit = diskLimit + /// 디스크 캐시는 일주일 제한 + cache.diskStorage.config.expiration = .days(7) + /// 100MB + let memoryLimit: Int = 100 * 1024 * 1024 + cache.memoryStorage.config.totalCostLimit = memoryLimit + } + private func setupOnboardingWhenTransferSuccessed(_ userInfo: [AnyHashable: Any]?) { // guard let infoDic = userInfo as? [String: Any] else { return } diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/DefinedError.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/DefinedError.swift index 0d018ec7..eee81cc8 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/DefinedError.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/DefinedError.swift @@ -17,6 +17,7 @@ enum DefinedError: Error, LocalizedError { case forbidden case notFound case teapot + case invlid case locked case invalidMethod(HTTPMethod) case unknown(Int) @@ -35,6 +36,8 @@ enum DefinedError: Error, LocalizedError { return .notFound case 418: return .teapot + case 422: + return .invlid case 423: return .locked default: @@ -56,6 +59,8 @@ enum DefinedError: Error, LocalizedError { return "Not Found: HTTP 404 received" case .teapot: return "Stop using RefreshToken: HTTP 418 received." + case .invlid: + return "Invlid Image: HTTP 422 received." case .locked: return "LOCKED: HTTP 423 received." case let .invalidMethod(httpMethod): @@ -73,6 +78,7 @@ enum DefinedError: Error, LocalizedError { case .forbidden: 403 case .notFound: 404 case .teapot: 418 + case .invlid: 422 case .locked: 423 case .invalidMethod: -99 case let .unknown(statusCode): statusCode diff --git a/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManagerConfiguration.swift b/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManagerConfiguration.swift index 0f561388..5deac8db 100644 --- a/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManagerConfiguration.swift +++ b/SOOUM/SOOUM/Data/Managers/NetworkManager/NetworkManagerConfiguration.swift @@ -38,10 +38,15 @@ struct NetworkManagerConfiguration: ManagerConfiguration { self.sessionDelegate = sessionDelegate self.sessionDelegateQueue = sessionDelegateQueue + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + formatter.locale = .Korea + formatter.timeZone = .Korea + self.decoder = JSONDecoder() - self.decoder.dateDecodingStrategy = .iso8601 + self.decoder.dateDecodingStrategy = .formatted(formatter) self.encoder = JSONEncoder() - self.encoder.dateEncodingStrategy = .iso8601 + self.encoder.dateEncodingStrategy = .formatted(formatter) } } diff --git a/SOOUM/SOOUM/Data/Models/Responses/BlockedNotificationInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/BlockedNotificationInfoResponse.swift index ce3fafea..1520eb63 100644 --- a/SOOUM/SOOUM/Data/Models/Responses/BlockedNotificationInfoResponse.swift +++ b/SOOUM/SOOUM/Data/Models/Responses/BlockedNotificationInfoResponse.swift @@ -7,7 +7,7 @@ import Alamofire -struct BlockedNotificationInfoResponse { +struct BlockedNotificationInfoResponse: Hashable, Equatable { let notificationInfo: CommonNotificationInfo let blockExpirationDateTime: Date diff --git a/SOOUM/SOOUM/Data/Models/Responses/CompositeNotificationInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/CompositeNotificationInfoResponse.swift new file mode 100644 index 00000000..501ec562 --- /dev/null +++ b/SOOUM/SOOUM/Data/Models/Responses/CompositeNotificationInfoResponse.swift @@ -0,0 +1,20 @@ +// +// CompositeNotificationInfoResponse.swift +// SOOUM +// +// Created by 오현식 on 9/26/25. +// + +import Alamofire + +struct CompositeNotificationInfoResponse: Decodable { + + let notificationInfo: [CompositeNotificationInfo] +} + +extension CompositeNotificationInfoResponse: EmptyResponse { + + static func emptyValue() -> CompositeNotificationInfoResponse { + CompositeNotificationInfoResponse(notificationInfo: []) + } +} diff --git a/SOOUM/SOOUM/Data/Models/Responses/DeleteNotificationInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/DeletedNotificationInfoResponse.swift similarity index 54% rename from SOOUM/SOOUM/Data/Models/Responses/DeleteNotificationInfoResponse.swift rename to SOOUM/SOOUM/Data/Models/Responses/DeletedNotificationInfoResponse.swift index 39b0a993..b6f98fb5 100644 --- a/SOOUM/SOOUM/Data/Models/Responses/DeleteNotificationInfoResponse.swift +++ b/SOOUM/SOOUM/Data/Models/Responses/DeletedNotificationInfoResponse.swift @@ -7,19 +7,19 @@ import Alamofire -struct DeleteNotificationInfoResponse { +struct DeletedNotificationInfoResponse: Hashable, Equatable { let notificationInfo: CommonNotificationInfo } -extension DeleteNotificationInfoResponse: EmptyResponse { +extension DeletedNotificationInfoResponse: EmptyResponse { - static func emptyValue() -> DeleteNotificationInfoResponse { - DeleteNotificationInfoResponse(notificationInfo: CommonNotificationInfo.defaultValue) + static func emptyValue() -> DeletedNotificationInfoResponse { + DeletedNotificationInfoResponse(notificationInfo: CommonNotificationInfo.defaultValue) } } -extension DeleteNotificationInfoResponse: Decodable { +extension DeletedNotificationInfoResponse: Decodable { init(from decoder: any Decoder) throws { let singleContainer = try decoder.singleValueContainer() diff --git a/SOOUM/SOOUM/Data/Models/Responses/FollowNotificationInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/FollowNotificationInfoResponse.swift new file mode 100644 index 00000000..b429f8ec --- /dev/null +++ b/SOOUM/SOOUM/Data/Models/Responses/FollowNotificationInfoResponse.swift @@ -0,0 +1,44 @@ +// +// FollowNotificationInfoResponse.swift +// SOOUM +// +// Created by 오현식 on 9/26/25. +// + +import Alamofire + +struct FollowNotificationInfoResponse: Hashable, Equatable { + + let notificationInfo: CommonNotificationInfo + let nickname: String + let userId: String +} + +extension FollowNotificationInfoResponse: EmptyResponse { + + static func emptyValue() -> FollowNotificationInfoResponse { + FollowNotificationInfoResponse( + notificationInfo: CommonNotificationInfo.defaultValue, + nickname: "", + userId: "" + ) + } +} + +extension FollowNotificationInfoResponse: Decodable { + + enum CodingKeys: CodingKey { + case notificationInfo + case nickname + case userId + } + + init(from decoder: any Decoder) throws { + let singleContainer = try decoder.singleValueContainer() + self.notificationInfo = try singleContainer.decode(CommonNotificationInfo.self) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.nickname = try container.decode(String.self, forKey: .nickname) + self.userId = String(try container.decode(Int64.self, forKey: .userId)) + } +} diff --git a/SOOUM/SOOUM/Data/Models/Responses/HomeCardInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/HomeCardInfoResponse.swift new file mode 100644 index 00000000..78320629 --- /dev/null +++ b/SOOUM/SOOUM/Data/Models/Responses/HomeCardInfoResponse.swift @@ -0,0 +1,32 @@ +// +// HomeCardInfoResponse.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Alamofire + +struct HomeCardInfoResponse { + + let cardInfos: [BaseCardInfo] +} + +extension HomeCardInfoResponse: EmptyResponse { + + static func emptyValue() -> HomeCardInfoResponse { + HomeCardInfoResponse(cardInfos: []) + } +} + +extension HomeCardInfoResponse: Decodable { + + enum CodingKeys: String, CodingKey { + case cardInfos + } + + init(from decoder: any Decoder) throws { + let singleContainer = try decoder.singleValueContainer() + self.cardInfos = try singleContainer.decode([BaseCardInfo].self) + } +} diff --git a/SOOUM/SOOUM/Data/Models/Responses/NoticeInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/NoticeInfoResponse.swift new file mode 100644 index 00000000..c940386e --- /dev/null +++ b/SOOUM/SOOUM/Data/Models/Responses/NoticeInfoResponse.swift @@ -0,0 +1,32 @@ +// +// NoticeInfoResponse.swift +// SOOUM +// +// Created by 오현식 on 9/26/25. +// + +import Alamofire + +struct NoticeInfoResponse { + + let noticeInfos: [NoticeInfo] +} + +extension NoticeInfoResponse: EmptyResponse { + + static func emptyValue() -> NoticeInfoResponse { + NoticeInfoResponse(noticeInfos: []) + } +} + +extension NoticeInfoResponse: Decodable { + + enum CodingKeys: String, CodingKey { + case noticeInfos + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.noticeInfos = try container.decode([NoticeInfo].self, forKey: .noticeInfos) + } +} diff --git a/SOOUM/SOOUM/Data/Models/Responses/NotificationInfoResponse.swift b/SOOUM/SOOUM/Data/Models/Responses/NotificationInfoResponse.swift index 4737c103..103faa2f 100644 --- a/SOOUM/SOOUM/Data/Models/Responses/NotificationInfoResponse.swift +++ b/SOOUM/SOOUM/Data/Models/Responses/NotificationInfoResponse.swift @@ -7,7 +7,7 @@ import Alamofire -struct NotificationInfoResponse { +struct NotificationInfoResponse: Hashable, Equatable { let notificationInfo: CommonNotificationInfo let targetCardId: String @@ -38,7 +38,7 @@ extension NotificationInfoResponse: Decodable { self.notificationInfo = try singleContainer.decode(CommonNotificationInfo.self) let container = try decoder.container(keyedBy: CodingKeys.self) - self.targetCardId = String(try container.decode(Int.self, forKey: .targetCardId)) + self.targetCardId = String(try container.decode(Int64.self, forKey: .targetCardId)) self.nickName = try container.decode(String.self, forKey: .nickName) } } diff --git a/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift new file mode 100644 index 00000000..238ecb1d --- /dev/null +++ b/SOOUM/SOOUM/Data/Repositories/CardRepositoryImpl.swift @@ -0,0 +1,34 @@ +// +// CardRepositoryImpl.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +import RxSwift + +class CardRepositoryImpl: CardRepository { + + private let remoteDataSource: CardRemoteDataSource + + init(remoteDataSource: CardRemoteDataSource) { + self.remoteDataSource = remoteDataSource + } + + func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable { + + return self.remoteDataSource.latestCard(lastId: lastId, latitude: latitude, longitude: longitude) + } + + func popularCard(latitude: String?, longitude: String?) -> Observable { + + return self.remoteDataSource.popularCard(latitude: latitude, longitude: longitude) + } + + func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable { + + return self.remoteDataSource.distanceCard(lastId: lastId, latitude: latitude, longitude: longitude, distanceFilter: distanceFilter) + } +} diff --git a/SOOUM/SOOUM/Data/Repositories/NotificationRepositoryImpl.swift b/SOOUM/SOOUM/Data/Repositories/NotificationRepositoryImpl.swift index ba93e710..031f686f 100644 --- a/SOOUM/SOOUM/Data/Repositories/NotificationRepositoryImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/NotificationRepositoryImpl.swift @@ -17,48 +17,23 @@ class NotificationRepositoryImpl: NotificationRepository { self.remoteDataSource = remoteDataSource } - func unreadNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func unreadNotifications(lastId: String?) -> Observable { return self.remoteDataSource.unreadNotifications(lastId: lastId) } - func unreadCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.remoteDataSource.unreadCardNotifications(lastId: lastId) - } - - func unreadFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.remoteDataSource.unreadFollowNotifications(lastId: lastId) - } - - func unreadNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.remoteDataSource.unreadNoticeNoticeNotifications(lastId: lastId) - } - - func readNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func readNotifications(lastId: String?) -> Observable { return self.remoteDataSource.readNotifications(lastId: lastId) } - func readCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.remoteDataSource.readCardNotifications(lastId: lastId) - } - - func readFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.remoteDataSource.readFollowNotifications(lastId: lastId) - } - - func readNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func requestRead(notificationId: String) -> Observable { - return self.remoteDataSource.readNoticeNoticeNotifications(lastId: lastId) + return self.remoteDataSource.requestRead(notificationId: notificationId) } - func requestRead(notificationId: String) -> Observable { + func notices(lastId: String?) -> Observable { - return self.remoteDataSource.requestRead(notificationId: notificationId) + return self.remoteDataSource.notices(lastId: lastId) } } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift new file mode 100644 index 00000000..dc649f0e --- /dev/null +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/CardRemoteDataSourceImpl.swift @@ -0,0 +1,37 @@ +// +// CardRemoteDataSourceImpl.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +import RxSwift + +class CardRemoteDataSourceImpl: CardRemoteDataSource { + + private let provider: ManagerProviderType + + init(provider: ManagerProviderType) { + self.provider = provider + } + + func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable { + + let request: CardRequest = .latestCard(lastId: lastId, latitude: latitude, longitude: longitude) + return self.provider.networkManager.fetch(HomeCardInfoResponse.self, request: request) + } + + func popularCard(latitude: String?, longitude: String?) -> Observable { + + let request: CardRequest = .popularCard(latitude: latitude, longitude: longitude) + return self.provider.networkManager.fetch(HomeCardInfoResponse.self, request: request) + } + + func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable { + + let request: CardRequest = .distancCard(lastId: lastId, latitude: latitude, longitude: longitude, distanceFilter: distanceFilter) + return self.provider.networkManager.fetch(HomeCardInfoResponse.self, request: request) + } +} diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift new file mode 100644 index 00000000..b140e50b --- /dev/null +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/CardRemoteDataSource.swift @@ -0,0 +1,17 @@ +// +// CardRemoteDataSource.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +import RxSwift + +protocol CardRemoteDataSource { + + func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable + func popularCard(latitude: String?, longitude: String?) -> Observable + func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable +} diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/NotificationRemoteDataSource.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/NotificationRemoteDataSource.swift index 73959f9c..f3d18b28 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/NotificationRemoteDataSource.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/Interfaces/NotificationRemoteDataSource.swift @@ -11,13 +11,8 @@ import RxSwift protocol NotificationRemoteDataSource { - func unreadNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> + func unreadNotifications(lastId: String?) -> Observable + func readNotifications(lastId: String?) -> Observable func requestRead(notificationId: String) -> Observable + func notices(lastId: String?) -> Observable } diff --git a/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift b/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift index b509768c..7637e4ed 100644 --- a/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift +++ b/SOOUM/SOOUM/Data/Repositories/Remotes/NotificationRemoteDataSoruceImpl.swift @@ -17,52 +17,16 @@ class NotificationRemoteDataSoruceImpl: NotificationRemoteDataSource { self.provider = provider } - func unreadNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func unreadNotifications(lastId: String?) -> Observable { let request: NotificationRequest = .unreadNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) + return self.provider.networkManager.fetch(CompositeNotificationInfoResponse.self, request: request) } - func unreadCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - let request: NotificationRequest = .unreadCardNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) - } - - func unreadFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - let request: NotificationRequest = .unreadFollowNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) - } - - func unreadNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - let request: NotificationRequest = .unreadNoticeNoticeNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) - } - - func readNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func readNotifications(lastId: String?) -> Observable { let request: NotificationRequest = .readNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) - } - - func readCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - let request: NotificationRequest = .readCardNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) - } - - func readFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - let request: NotificationRequest = .readFollowNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) - } - - func readNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - let request: NotificationRequest = .readNoticeNoticeNotifications(lastId: lastId) - return self.provider.networkManager.fetch([NotificationInfoResponse].self, request: request) + return self.provider.networkManager.fetch(CompositeNotificationInfoResponse.self, request: request) } func requestRead(notificationId: String) -> Observable { @@ -70,4 +34,10 @@ class NotificationRemoteDataSoruceImpl: NotificationRemoteDataSource { let request: NotificationRequest = .requestRead(notificationId: notificationId) return self.provider.networkManager.perform(Int.self, request: request) } + + func notices(lastId: String?) -> Observable { + + let request: NotificationRequest = .notices(lastId: lastId) + return self.provider.networkManager.fetch(NoticeInfoResponse.self, request: request) + } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift new file mode 100644 index 00000000..d70bacac --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMCard.swift @@ -0,0 +1,545 @@ +// +// SOMCard.swift +// SOOUM +// +// Created by JDeoks on 9/14/24. +// + +import UIKit + +import SnapKit +import Then + +import RxSwift + +class SOMCard: UIView { + + enum Text { + static let adminTitle: String = "sooum" + static let pungedCardText: String = "카드가 삭제되었어요" + } + + + // MARK: Views + + let shadowbackgroundView = UIView().then { + $0.backgroundColor = .som.v2.white + $0.layer.cornerRadius = 16 + } + + /// 배경 이미지 + let rootContainerImageView = UIImageView().then { + $0.layer.cornerRadius = 16 + $0.layer.borderWidth = 1 + $0.contentMode = .scaleAspectFill + $0.layer.masksToBounds = true + } + + // 본문 dim 배경 + let cardTextBackgroundBlurView = UIView().then { + $0.backgroundColor = .som.v2.dim + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + } + + /// 본문 표시 라벨 (스크롤 X) + let cardTextContentLabel = UILabel().then { + $0.textColor = .som.v2.white + $0.typography = .som.v2.body1 + $0.textAlignment = .center + $0.numberOfLines = 3 + $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 { + $0.backgroundColor = .som.v2.white + $0.layer.borderColor = UIColor.som.v2.white.cgColor + $0.layer.borderWidth = 1 + } + /// 펑 시간, 거리, 시간을 담는 스택 뷰 + let cardInfoLeadingStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 4 + $0.alignment = .center + } + /// 좋아요 수, 답글 수를 담는 스택 뷰 + let cardInfoTrailingStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 4 + $0.alignment = .center + } + + /// 어드민 정보 표시 스택뷰 + let adminStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 2 + $0.alignment = .center + } + /// 어드민 정보 아이콘 + let adminImageView = UIImageView().then { + $0.image = .init(.icon(.v2(.filled(.official)))) + $0.tintColor = .som.v2.black + } + /// 어드민 정보 라벨 + let adminLabel = UILabel().then { + $0.text = Text.adminTitle + $0.textColor = .som.v2.black + $0.typography = .som.v2.caption2 + } + /// 어드민 닷 + let firstDot = UIView().then { + $0.backgroundColor = .som.v2.gray500 + $0.layer.cornerRadius = 1 + } + /// 펑 남은시간 표시 스택뷰 + let cardPungTimeStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 2 + $0.alignment = .center + } + /// 펑 남은시간 표시 아이콘 + let cardPungTimeImageView = UIImageView().then { + $0.image = .init(.icon(.v2(.filled(.bomb)))) + $0.tintColor = .som.v2.pMain + } + /// 펑 남은시간 표시 라벨 + let cardPungTimeLabel = UILabel().then { + $0.textColor = .som.v2.pDark + $0.typography = .som.v2.caption2 + } + /// 펑 남은시간 닷 + let secondDot = UIView().then { + $0.backgroundColor = .som.v2.gray500 + $0.layer.cornerRadius = 1 + } + /// 거리 정보 표시 스택뷰 + let distanceInfoStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 2 + $0.alignment = .center + } + /// 거리 정보 아이콘 + let distanceImageView = UIImageView().then { + $0.image = .init(.icon(.v2(.outlined(.location)))) + $0.tintColor = .som.v2.gray500 + } + /// 거리 정보 라벨 + let distanceLabel = UILabel().then { + $0.textColor = .som.v2.gray500 + $0.typography = .som.v2.caption2 + } + /// 거리 정보 닷 + let thirdDot = UIView().then { + $0.backgroundColor = .som.v2.gray500 + $0.layer.cornerRadius = 1 + } + /// 시간 정보 표시 라벨 + let timeLabel = UILabel().then { + $0.textColor = .som.v2.gray500 + $0.typography = .som.v2.caption2 + } + /// 좋아요 정보 표시 스택뷰 + let likeInfoStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 2 + $0.alignment = .center + } + /// 좋아요 정보 표시 아이콘 + let likeImageView = UIImageView().then { + $0.image = .init(.icon(.v2(.outlined(.heart)))) + $0.tintColor = .som.v2.gray500 + } + /// 좋아요 정보 표시 라벨 + let likeLabel = UILabel().then { + $0.textColor = .som.v2.gray500 + $0.typography = .som.v2.caption2 + } + /// 답카드 정보 표시 스택뷰 + let commentInfoStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 2 + $0.alignment = .center + } + /// 답카드 정보 표시 아이콘 + let commentImageView = UIImageView().then { + $0.image = .init(.icon(.v2(.outlined(.message_circle)))) + $0.tintColor = .som.v2.gray500 + } + /// 답카드 정보 표시 라벨 + let commentLabel = UILabel().then { + $0.textColor = .som.v2.gray500 + $0.typography = .som.v2.caption2 + } + + + // MARK: Variables + + var model: BaseCardInfo? + + private var hasScrollEnabled: Bool + + + // MARK: Constraints + + // TODO: 카드 본문 배경 블러 뷰 높이 계산 Constraint, 헌재 사용 X + private var contentHeightConstraint: Constraint? + private var scrollContentHieghtConstraint: Constraint? + + /// 펑 이벤트 처리 위해 추가 + var serialTimer: Disposable? + var disposeBag = DisposeBag() + + + // MARK: Initialize + + init(hasScrollEnabled: Bool = false) { + self.hasScrollEnabled = hasScrollEnabled + super.init(frame: .zero) + + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Override func + + override func layoutSubviews() { + super.layoutSubviews() + + self.shadowbackgroundView.setShadow( + radius: 6, + color: UIColor(hex: "#ABBED11A").withAlphaComponent(0.1), + blur: 16, + offset: .init(width: 0, height: 6) + ) + } + + + // MARK: private func + + private func setupConstraints() { + + // 백그라운드 그림자 뷰 + self.addSubview(self.shadowbackgroundView) + self.shadowbackgroundView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + // 배경 이미지 뷰 + self.addSubview(self.rootContainerImageView) + self.rootContainerImageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + // 하단 카드 정보 컨테이너 + self.rootContainerImageView.addSubview(self.cardInfoContainer) + self.cardInfoContainer.snp.makeConstraints { + $0.bottom.horizontalEdges.equalToSuperview() + $0.height.equalTo(34) + } + + // 좌측 + self.cardInfoContainer.addSubview(self.cardInfoLeadingStackView) + self.cardInfoLeadingStackView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().offset(16) + $0.height.equalTo(18) + } + + self.adminStackView.addArrangedSubview(self.adminImageView) + self.adminStackView.addArrangedSubview(self.adminLabel) + self.adminImageView.snp.makeConstraints { + $0.size.equalTo(16) + } + + self.cardPungTimeStackView.addArrangedSubview(self.cardPungTimeImageView) + self.cardPungTimeStackView.addArrangedSubview(self.cardPungTimeLabel) + self.cardPungTimeImageView.snp.makeConstraints { + $0.size.equalTo(16) + } + + self.distanceInfoStackView.addArrangedSubview(self.distanceImageView) + self.distanceInfoStackView.addArrangedSubview(self.distanceLabel) + self.distanceImageView.snp.makeConstraints { + $0.size.equalTo(14) + } + + self.cardInfoLeadingStackView.addArrangedSubview(self.adminStackView) + self.cardInfoLeadingStackView.addArrangedSubview(self.firstDot) + self.firstDot.snp.makeConstraints { + $0.size.equalTo(2) + } + + self.cardInfoLeadingStackView.addArrangedSubview(self.cardPungTimeStackView) + self.cardInfoLeadingStackView.addArrangedSubview(self.secondDot) + self.secondDot.snp.makeConstraints { + $0.size.equalTo(2) + } + + self.cardInfoLeadingStackView.addArrangedSubview(self.distanceInfoStackView) + self.cardInfoLeadingStackView.addArrangedSubview(self.thirdDot) + self.thirdDot.snp.makeConstraints { + $0.size.equalTo(2) + } + self.cardInfoLeadingStackView.addArrangedSubview(self.timeLabel) + + // 우측 + self.cardInfoContainer.addSubview(self.cardInfoTrailingStackView) + self.cardInfoTrailingStackView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.greaterThanOrEqualTo(self.cardInfoLeadingStackView.snp.trailing).offset(4) + $0.trailing.equalToSuperview().offset(-16) + $0.height.equalTo(18) + } + + self.likeInfoStackView.addArrangedSubview(self.likeImageView) + self.likeInfoStackView.addArrangedSubview(self.likeLabel) + self.likeImageView.snp.makeConstraints { + $0.size.equalTo(14) + } + + self.commentInfoStackView.addArrangedSubview(self.commentImageView) + self.commentInfoStackView.addArrangedSubview(self.commentLabel) + self.commentImageView.snp.makeConstraints { + $0.size.equalTo(14) + } + + self.cardInfoTrailingStackView.addArrangedSubview(self.likeInfoStackView) + self.cardInfoTrailingStackView.addArrangedSubview(self.commentInfoStackView) + + // 카드 문구 + self.rootContainerImageView.addSubview(self.cardTextBackgroundBlurView) + self.cardTextBackgroundBlurView.snp.makeConstraints { + $0.centerY.equalToSuperview().offset(-34 * 0.5) + $0.leading.equalToSuperview().offset(32) + $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) + } + } + } + + + // MARK: Public func + + /// 이 컴포넌트를 사용하는 재사용 셀에서 호출 + func prepareForReuse() { + self.serialTimer?.dispose() + self.disposeBag = DisposeBag() + } + + /// 홈피드 모델 초기화 + func setModel(model: BaseCardInfo) { + + self.model = model + // 카드 배경 이미지 + self.rootContainerImageView.setImage(strUrl: model.cardImgURL, with: model.cardImgName) + self.rootContainerImageView.layer.borderColor = model.isAdminCard ? UIColor.som.v2.pMain.cgColor : UIColor.som.v2.gray100.cgColor + + // 카드 본문 + // TODO: 임시, 폰트 추가되면 수정 + let typography: Typography = model.font == .pretendard ? .som.v2.body1 : .som.schoolBody1WithBold + if self.hasScrollEnabled { + self.cardTextContentScrollView.attributedText = .init( + string: model.cardContent, + attributes: typography.attributes + ) + self.cardTextContentScrollView.typography = typography + } else { + self.cardTextContentLabel.attributedText = .init( + string: model.cardContent, + attributes: typography.attributes + ) + self.cardTextContentLabel.typography = typography + } + + // 하단 정보 + // 어드민, 펑 시간, 거리, 시간 + self.adminStackView.isHidden = model.isAdminCard == false + self.firstDot.isHidden = model.isAdminCard == false + self.cardPungTimeStackView.isHidden = model.storyExpirationTime == nil + self.secondDot.isHidden = model.storyExpirationTime == nil + self.distanceLabel.text = model.distance + self.distanceInfoStackView.isHidden = model.distance == nil + self.thirdDot.isHidden = model.distance == nil + self.timeLabel.text = model.createdAt.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) + + // 좋아요 수, 답글 수 + let likeText = model.likeCnt > 99 ? "99+" : "\(model.likeCnt)" + self.likeLabel.attributedText = .init(string: likeText, attributes: Typography.som.v2.caption2.attributes) + + let commentText = model.commentCnt > 99 ? "99+" : "\(model.commentCnt)" + self.commentLabel.attributedText = .init(string: commentText, attributes: Typography.som.v2.caption2.attributes) + + // 스토리 정보 설정 + self.subscribePungTime(model.storyExpirationTime) + } + + func setData(tagCard: TagDetailCardResponse.TagFeedCard) { + + // 카드 배경 이미지 +// rootContainerImageView.setImage(strUrl: tagCard.backgroundImgURL.href) +// // 카드 본문 +// updateContentHeight(tagCard.content) +// let typography: Typography = tagCard.font == .pretendard ? .som.body1WithBold : .som.schoolBody1WithBold +// if hasScrollEnabled { +// var attributes = typography.attributes +// attributes.updateValue(typography.font, forKey: .font) +// attributes.updateValue(UIColor.som.white, forKey: .foregroundColor) +// cardTextContentScrollView.attributedText = .init( +// string: tagCard.content, +// attributes: attributes +// ) +// cardTextContentScrollView.textAlignment = .center +// } else { +// cardTextContentLabel.typography = typography +// cardTextContentLabel.text = tagCard.content +// cardTextContentLabel.textAlignment = .center +// cardTextContentLabel.lineBreakMode = .byTruncatingTail +// } +// // 하단 정보 +// likeImageView.image = tagCard.isLiked ? +// .init(.icon(.filled(.heart))) : +// .init(.icon(.outlined(.heart))) +// likeImageView.tintColor = tagCard.isLiked ? .som.p300 : .som.white +// +// commentImageView.image = tagCard.isCommentWritten ? +// .init(.icon(.filled(.comment))) : +// .init(.icon(.outlined(.comment))) +// commentImageView.tintColor = tagCard.isCommentWritten ? .som.p300 : .som.white +// +// timeLabel.text = tagCard.createdAt.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) +// distanceInfoStackView.isHidden = tagCard.distance == nil +// distanceLabel.text = (tagCard.distance ?? 0).infoReadableDistanceRangeFromThis() +// likeLabel.text = "\(tagCard.likeCnt)" +// likeLabel.textColor = tagCard.isLiked ? .som.p300 : .som.white +// commentLabel.text = "\(tagCard.commentCnt)" +// commentLabel.textColor = tagCard.isCommentWritten ? .som.p300 : .som.white +// +// cardPungTimeBackgroundView.isHidden = true + } + + // TODO: 카드 본문 배경 블러 뷰 높이 계산 함수, 헌재 사용 X + private func updateContentHeight(_ text: String) { + + self.layoutIfNeeded() + // TODO: 임시, 폰트 가변임 + let typography = Typography.som.v2.body1 + 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 + let size: CGSize = .init(width: availableWidth, height: .greatestFiniteMagnitude) + let boundingRect = attributedText.boundingRect( + with: size, + options: [.usesLineFragmentOrigin], + context: nil + ) + let boundingHeight = boundingRect.height + 20 * 2 /// top, bottom inset + let backgroundHeight = rootContainerImageView.bounds.height + + let height = min(boundingHeight, (backgroundHeight - 34) * 0.8) + + 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 + ) + } + } + + + // MARK: - 카드 펑 로직 + + /// 펑 이벤트 구독 + private func subscribePungTime(_ pungTime: Date?) { + self.serialTimer?.dispose() + self.serialTimer = Observable.interval(.seconds(1), scheduler: MainScheduler.instance) + .withUnretained(self) + .startWith((self, 0)) + .map { object, _ in + guard let pungTime = pungTime else { + object.serialTimer?.dispose() + return "00 : 00 : 00" + } + + let currentDate = Date() + let remainingTime = currentDate.infoReadableTimeTakenFromThisForPung(to: pungTime) + if remainingTime == "00 : 00 : 00" { + object.serialTimer?.dispose() + object.updatePungUI() + } + + return remainingTime + } + .bind(to: self.cardPungTimeLabel.rx.text) + } + + /// 펑 ui 즉각적으로 업데이트 + private func updatePungUI() { + self.cardPungTimeLabel.text = "00 : 00 : 00" + self.rootContainerImageView.layer.borderWidth = 0 + self.rootContainerImageView.image = UIColor.som.v2.gray200.toImage + self.cardInfoContainer.subviews + .filter { $0 != self.cardInfoLeadingStackView } + .forEach { $0.removeFromSuperview() } + self.cardInfoLeadingStackView.subviews + .filter { $0 != self.cardPungTimeStackView } + .forEach { $0.removeFromSuperview() } + + if self.hasScrollEnabled { + self.cardTextContentScrollView.text = Text.pungedCardText + } else { + self.cardTextContentLabel.text = Text.pungedCardText + } + } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMCard/SOMCard.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMCard/SOMCard.swift deleted file mode 100644 index 481a3dd7..00000000 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMCard/SOMCard.swift +++ /dev/null @@ -1,579 +0,0 @@ -// -// SOMCard.swift -// SOOUM -// -// Created by JDeoks on 9/14/24. -// - -import UIKit - -import RxSwift -import SnapKit -import Then - - -class SOMCard: UIView { - - enum Text { - static let pungedCardInMainHomeText: String = "삭제된 카드에요" - } - - var model: SOMCardModel? - - private var hasScrollEnabled: Bool - - private var contentHeightConstraint: Constraint? - private var scrollContentHieghtConstraint: Constraint? - - /// 펑 이벤트 처리 위해 추가 - var serialTimer: Disposable? - var disposeBag = DisposeBag() - - /// 배경 이미지 - let rootContainerImageView = UIImageView().then { - $0.backgroundColor = .clear - $0.layer.cornerRadius = 40 - $0.layer.masksToBounds = true - } - - /// 카드 펑 라벨 배경 - let cardPungTimeBackgroundView = UIView().then { - $0.backgroundColor = .som.blue300 - $0.layer.cornerRadius = 12 - $0.layer.masksToBounds = true - } - /// 카드 펑 남은시간 표시 라벨 - let cardPungTimeLabel = UILabel().then { - $0.typography = .som.body2WithBold - $0.textColor = .som.white - $0.textAlignment = .center - } - - /// pungTime != nil - /// 삭제(펑 됐을 때) 배경 - let pungedCardInMainHomeBackgroundView = UIView().then { - $0.backgroundColor = UIColor(hex: "#303030").withAlphaComponent(0.7) - $0.layer.cornerRadius = 40 - $0.isHidden = true - } - /// 삭제(펑 됐을 때) 라벨 - let pungedCardInMainHomeLabel = UILabel().then { - $0.text = Text.pungedCardInMainHomeText - $0.textColor = .som.white - $0.textAlignment = .center - $0.typography = .som.body1WithBold - } - - /// 본문을 감싸는 불투명 컨테이너 뷰 - let cardTextBackgroundBlurView = UIVisualEffectView().then { - let blurEffect = UIBlurEffect(style: .dark) - $0.effect = blurEffect - $0.backgroundColor = .som.dim - $0.alpha = 0.8 - $0.layer.cornerRadius = 24 - $0.clipsToBounds = true - } - /// 본문 표시 라벨 (스크롤 X) - let cardTextContentLabel = UILabel().then { - $0.textColor = .som.white - $0.textAlignment = .center - $0.numberOfLines = 0 - $0.lineBreakMode = .byTruncatingTail - $0.typography = .som.body1WithBold - } - /// 본문 스크롤 텍스트 뷰 (스크롤 O) - let cardTextContentScrollView = UITextView().then { - $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 cardGradientView = UIView().then { - $0.backgroundColor = .clear - } - - let cardGradientLayer = CAGradientLayer() - - /// 좋아요, 거리, 답카드, 시간 정보 포함하는 스택뷰 - let cardContentStackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 8 - } - - /// 시간 정보 표시 스택뷰 - let timeInfoStackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 4 - } - - let timeImageView = UIImageView().then { - $0.image = .init(.icon(.outlined(.clock))) - $0.tintColor = .som.white - } - - let timeLabel = UILabel().then { - $0.typography = .som.body3WithRegular - $0.textColor = .som.white - } - - /// 거리 정보 표시 스택뷰 - let distanceInfoStackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 4 - } - - let distanceImageView = UIImageView().then { - $0.image = .init(.icon(.outlined(.location))) - $0.tintColor = .som.white - } - - let distanceLabel = UILabel().then { - $0.typography = .som.body3WithRegular - $0.textColor = .som.white - } - - /// 좋아요 정보 표시 스택뷰 - let likeInfoStackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 4 - } - - let likeImageView = UIImageView().then { - $0.image = .init(.icon(.outlined(.heart))) - $0.tintColor = .som.white - } - - let likeLabel = UILabel().then { - $0.typography = .som.body3WithRegular - $0.textColor = .som.white - } - - /// 답카드 정보 표시 스택뷰 - let commentInfoStackView = UIStackView().then { - $0.axis = .horizontal - $0.spacing = 4 - } - - let commentImageView = UIImageView().then { - $0.image = .init(.icon(.outlined(.comment))) - $0.tintColor = .som.white - } - - let commentLabel = UILabel().then { - $0.typography = .som.body3WithRegular - $0.textColor = .som.white - } - - - // MARK: - init - init(hasScrollEnabled: Bool = false) { - self.hasScrollEnabled = hasScrollEnabled - super.init(frame: .zero) - initUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - DispatchQueue.main.async { [weak self] in - self?.setGradientLayerFrame() - } - } - - /// 이 컴포넌트를 사용하는 재사용 셀에서 호출 - func prepareForReuse() { - serialTimer?.dispose() - disposeBag = DisposeBag() - } - - // MARK: - initUI - private func initUI() { - addSubviews() - initConstraint() - addGradient() - } - - private func addSubviews() { - self.addSubview(rootContainerImageView) - addPungedCardInMainHomeView() - addCardPungTimeLabel() - addCardTextContainerView() - addCardGradientView() - addCardContentStackView() - } - - private func addPungedCardInMainHomeView() { - self.addSubview(pungedCardInMainHomeBackgroundView) - pungedCardInMainHomeBackgroundView.addSubview(pungedCardInMainHomeLabel) - } - - private func addCardPungTimeLabel() { - rootContainerImageView.addSubview(cardPungTimeBackgroundView) - cardPungTimeBackgroundView.addSubview(cardPungTimeLabel) - } - - private func addCardTextContainerView() { - self.addSubview(cardTextBackgroundBlurView) - if hasScrollEnabled { - self.addSubview(cardTextContentScrollView) - } else { - self.addSubview(cardTextContentLabel) - } - } - - private func addCardContentStackView() { - rootContainerImageView.addSubview(cardContentStackView) - - cardContentStackView.addArrangedSubviews( - UIView(), - timeInfoStackView, - distanceInfoStackView, - likeInfoStackView, - commentInfoStackView - ) - - addTimeInfoStackView() - addDistanceInfoStackView() - addLikeInfoStackView() - addCommentInfoStackView() - } - - private func addCardGradientView() { - rootContainerImageView.addSubview(cardGradientView) - rootContainerImageView.bringSubviewToFront(cardGradientView) - } - - private func addTimeInfoStackView() { - timeInfoStackView.addArrangedSubviews(timeImageView, timeLabel) - } - - private func addDistanceInfoStackView() { - distanceInfoStackView.addArrangedSubviews(distanceImageView, distanceLabel) - } - - private func addLikeInfoStackView() { - likeInfoStackView.addArrangedSubviews(likeImageView, likeLabel) - } - - private func addCommentInfoStackView() { - commentInfoStackView.addArrangedSubviews(commentImageView, commentLabel) - } - - - // MARK: - initConstraint - - private func initConstraint() { - /// 홈피드 이미지 배경 - rootContainerImageView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - /// 삭제(펑 됐을 때) 라벨 - pungedCardInMainHomeBackgroundView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - pungedCardInMainHomeLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - - /// 펑 라벨 - cardPungTimeBackgroundView.snp.makeConstraints { - $0.top.equalToSuperview().offset(26) - $0.centerX.equalToSuperview() - $0.height.equalTo(25) - } - cardPungTimeLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.trailing.equalToSuperview().inset(10) - } - - /// 본문 라벨 - cardTextBackgroundBlurView.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalToSuperview().offset(40) - $0.trailing.equalToSuperview().offset(-40) - contentHeightConstraint = $0.height.equalTo(24 + 14 * 2).priority(.high).constraint - } - if hasScrollEnabled { - cardTextContentScrollView.snp.makeConstraints { - $0.top.equalTo(cardTextBackgroundBlurView.snp.top).offset(14) - $0.bottom.equalTo(cardTextBackgroundBlurView.snp.bottom).offset(-14) - $0.leading.equalTo(cardTextBackgroundBlurView.snp.leading) - $0.trailing.equalTo(cardTextBackgroundBlurView.snp.trailing) - } - } else { - cardTextContentLabel.snp.makeConstraints { - $0.top.equalTo(cardTextBackgroundBlurView.snp.top).offset(14) - $0.bottom.equalTo(cardTextBackgroundBlurView.snp.bottom).offset(-14) - $0.leading.equalTo(cardTextBackgroundBlurView.snp.leading).offset(16) - $0.trailing.equalTo(cardTextBackgroundBlurView.snp.trailing).offset(-16) - } - } - - /// 하단 그라디언트 뷰 - cardGradientView.snp.makeConstraints { - $0.bottom.leading.trailing.equalToSuperview() - $0.height.equalTo(60) - } - - /// 하단 컨텐트 뷰 - cardContentStackView.snp.makeConstraints { - $0.bottom.equalToSuperview().offset(-24) - $0.leading.equalToSuperview() - $0.trailing.equalToSuperview().offset(-26) - $0.height.height.equalTo(12) - } - timeImageView.snp.makeConstraints { - $0.height.width.equalTo(12) - } - distanceImageView.snp.makeConstraints { - $0.height.width.equalTo(12) - } - likeImageView.snp.makeConstraints { - $0.height.width.equalTo(12) - } - commentImageView.snp.makeConstraints { - $0.height.width.equalTo(12) - } - } - - /// cardGradientLayer - private func addGradient() { - - cardGradientLayer.colors = [ - UIColor.clear.cgColor, - UIColor.black.withAlphaComponent(0.6).cgColor - ] - cardGradientLayer.startPoint = CGPoint(x: 0.5, y: 0) - cardGradientLayer.endPoint = CGPoint(x: 0.5, y: 1) - cardGradientView.layer.insertSublayer(cardGradientLayer, at: 0) - } - - private func setGradientLayerFrame() { - - CATransaction.begin() - CATransaction.setDisableActions(true) - cardGradientLayer.frame = cardGradientView.bounds - CATransaction.commit() - } - - /// 홈피드 모델 초기화 - func setModel(model: SOMCardModel) { - - self.model = model - // 카드 배경 이미지 - rootContainerImageView.setImage(strUrl: model.data.backgroundImgURL.url) - - // 카드 본문 - updateContentHeight(model.data.content) - let typography: Typography = model.data.font == .pretendard ? .som.body1WithBold : .som.schoolBody1WithBold - if hasScrollEnabled { - var attributes = typography.attributes - attributes.updateValue(typography.font, forKey: .font) - attributes.updateValue(UIColor.som.white, forKey: .foregroundColor) - cardTextContentScrollView.attributedText = .init( - string: model.data.content, - attributes: attributes - ) - cardTextContentScrollView.textAlignment = .center - } else { - cardTextContentLabel.typography = typography - cardTextContentLabel.text = model.data.content - cardTextContentLabel.textAlignment = .center - cardTextContentLabel.lineBreakMode = .byTruncatingTail - } - - // 하단 정보 - likeImageView.image = model.data.isLiked ? - .init(.icon(.filled(.heart))) : - .init(.icon(.outlined(.heart))) - likeImageView.tintColor = model.data.isLiked ? .som.p300 : .som.white - commentImageView.image = model.data.isCommentWritten ? - .init(.icon(.filled(.comment))) : - .init(.icon(.outlined(.comment))) - commentImageView.tintColor = model.data.isCommentWritten ? .som.p300 : .som.white - - timeLabel.text = model.data.createdAt.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) - distanceInfoStackView.isHidden = model.data.distance == nil - distanceLabel.text = (model.data.distance ?? 0).infoReadableDistanceRangeFromThis() - likeLabel.text = model.data.likeCnt > 99 ? "99+" : "\(model.data.likeCnt)" - likeLabel.textColor = model.data.isLiked ? .som.p300 : .som.white - commentLabel.text = model.data.commentCnt > 99 ? "99+" : "\(model.data.commentCnt)" - commentLabel.textColor = model.data.isCommentWritten ? .som.p300 : .som.white - - // 스토리 정보 설정 - cardPungTimeBackgroundView.isHidden = model.data.storyExpirationTime == nil - self.subscribePungTime() - } - - func setData(tagCard: TagDetailCardResponse.TagFeedCard) { - - // 카드 배경 이미지 - rootContainerImageView.setImage(strUrl: tagCard.backgroundImgURL.href) - // 카드 본문 - updateContentHeight(tagCard.content) - let typography: Typography = tagCard.font == .pretendard ? .som.body1WithBold : .som.schoolBody1WithBold - if hasScrollEnabled { - var attributes = typography.attributes - attributes.updateValue(typography.font, forKey: .font) - attributes.updateValue(UIColor.som.white, forKey: .foregroundColor) - cardTextContentScrollView.attributedText = .init( - string: tagCard.content, - attributes: attributes - ) - cardTextContentScrollView.textAlignment = .center - } else { - cardTextContentLabel.typography = typography - cardTextContentLabel.text = tagCard.content - cardTextContentLabel.textAlignment = .center - cardTextContentLabel.lineBreakMode = .byTruncatingTail - } - // 하단 정보 - likeImageView.image = tagCard.isLiked ? - .init(.icon(.filled(.heart))) : - .init(.icon(.outlined(.heart))) - likeImageView.tintColor = tagCard.isLiked ? .som.p300 : .som.white - - commentImageView.image = tagCard.isCommentWritten ? - .init(.icon(.filled(.comment))) : - .init(.icon(.outlined(.comment))) - commentImageView.tintColor = tagCard.isCommentWritten ? .som.p300 : .som.white - - timeLabel.text = tagCard.createdAt.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) - distanceInfoStackView.isHidden = tagCard.distance == nil - distanceLabel.text = (tagCard.distance ?? 0).infoReadableDistanceRangeFromThis() - likeLabel.text = "\(tagCard.likeCnt)" - likeLabel.textColor = tagCard.isLiked ? .som.p300 : .som.white - commentLabel.text = "\(tagCard.commentCnt)" - commentLabel.textColor = tagCard.isCommentWritten ? .som.p300 : .som.white - - cardPungTimeBackgroundView.isHidden = true - } - - /// 카드 모드에 따라 스택뷰 순서 변경 - func changeOrderInCardContentStack(_ selectedIndex: Int) { - cardContentStackView.subviews.forEach { $0.removeFromSuperview() } - - switch selectedIndex { - case 1: - cardContentStackView.addArrangedSubviews( - UIView(), - likeInfoStackView, - commentInfoStackView, - timeInfoStackView, - distanceInfoStackView - ) - case 2: - cardContentStackView.addArrangedSubviews( - UIView(), - distanceInfoStackView, - timeInfoStackView, - likeInfoStackView, - commentInfoStackView - ) - default: - cardContentStackView.addArrangedSubviews( - UIView(), - timeInfoStackView, - distanceInfoStackView, - likeInfoStackView, - commentInfoStackView - ) - } - } - - // 상세보기 일 때, 좋아요, 코맨트 제거 - func removeLikeAndCommentInStack() { - - cardContentStackView.subviews.forEach { $0.removeFromSuperview() } - cardContentStackView.addArrangedSubviews(UIView(), distanceInfoStackView, timeInfoStackView) - } - - private func updateContentHeight(_ text: String) { - - layoutIfNeeded() - - let typography = Typography.som.body1WithBold - var attributes = typography.attributes - attributes.updateValue(typography.font, forKey: .font) - let attributedText = NSAttributedString( - string: text, - attributes: attributes - ) - - let availableWidth = UIScreen.main.bounds.width - 20 * 2 - 40 * 2 - 16 * 2 - let size: CGSize = .init(width: availableWidth, height: .greatestFiniteMagnitude) - let boundingRect = attributedText.boundingRect( - with: size, - options: [.usesLineFragmentOrigin], - context: nil - ) - let boundingHeight = boundingRect.height + 14 * 2 /// top, bottom inset - let backgroundHeight = rootContainerImageView.bounds.height - - let height = min(boundingHeight, backgroundHeight * 0.5) - - contentHeightConstraint?.deactivate() - cardTextBackgroundBlurView.snp.makeConstraints { - contentHeightConstraint = $0.height.equalTo(height).priority(.high).constraint - } - - if hasScrollEnabled { - cardTextContentScrollView.isScrollEnabled = boundingHeight > backgroundHeight * 0.5 - cardTextContentScrollView.isUserInteractionEnabled = true - cardTextContentScrollView.contentSize = .init( - width: cardTextContentScrollView.bounds.width, - height: boundingHeight - ) - } - } - - - // MARK: - 카드 펑 로직 - - /// 펑 이벤트 구독 - private func subscribePungTime() { - self.serialTimer?.dispose() - self.serialTimer = Observable.interval(.seconds(1), scheduler: MainScheduler.instance) - .withUnretained(self) - .startWith((self, 0)) - .map { object, _ in - guard let pungTime = object.model?.pungTime else { - object.serialTimer?.dispose() - return "00 : 00 : 00" - } - - let currentDate = Date() - let remainingTime = currentDate.infoReadableTimeTakenFromThisForPung(to: pungTime) - if remainingTime == "00 : 00 : 00" { - object.serialTimer?.dispose() - object.updatePungUI() - } - - return remainingTime - } - .bind(to: self.cardPungTimeLabel.rx.text) - } - - /// 펑 ui 즉각적으로 업데이트 - private func updatePungUI() { - rootContainerImageView.subviews.forEach { $0.removeFromSuperview() } - pungedCardInMainHomeBackgroundView.isHidden = false - } -} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMCard/SOMCardModel.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMCard/SOMCardModel.swift deleted file mode 100644 index 19b749a9..00000000 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMCard/SOMCardModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SOMCardModel.swift -// SOOUM -// -// Created by 오현식 on 10/3/24. -// - -import Foundation - - -struct SOMCardModel { - - /// 카드 정보 - let data: Card - /// 스토리 펑타임 - var pungTime: Date? - /// 현재 카드가 펑된 카드인지 확인 - var isPunged: Bool { - guard let pungTime = self.data.storyExpirationTime else { return false } - let remainingTime: TimeInterval = pungTime.timeIntervalSinceNow - return remainingTime <= 0.0 - } - - init(data: Card) { - self.data = data - self.pungTime = data.storyExpirationTime - } -} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMDialogController/SOMDialogAction.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMDialogController/SOMDialogAction.swift index c5475e38..97deeede 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMDialogController/SOMDialogAction.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMDialogController/SOMDialogAction.swift @@ -19,7 +19,7 @@ class SOMDialogAction { case .primary: return .som.v2.black case .gray: - return .som.gray300 + return .som.v2.gray100 } } @@ -28,7 +28,7 @@ class SOMDialogAction { case .primary: return .som.v2.white case .gray: - return .som.gray700 + return .som.v2.gray600 } } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMLoadingIndicatorView.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMLoadingIndicatorView.swift index b303d1d0..1115ab59 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMLoadingIndicatorView.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMLoadingIndicatorView.swift @@ -70,6 +70,7 @@ class SOMLoadingIndicatorView: UIView { self.backgroundView.addSubviews(self.animationView) self.animationView.snp.makeConstraints { $0.center.equalToSuperview() + $0.size.equalTo(60) } } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMLocationFilter/SOMLocationFilter.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMLocationFilter/SOMLocationFilter.swift deleted file mode 100644 index 69df9b9a..00000000 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMLocationFilter/SOMLocationFilter.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// SOMLocationFilter.swift -// SOOUM -// -// Created by JDeoks on 9/19/24. -// - -import UIKit - -import SnapKit -import Then - -protocol SOMLocationFilterDelegate: AnyObject { - - func filter( - _ filter: SOMLocationFilter, - didSelectDistanceAt distance: SOMLocationFilter.Distance - ) -} - -class SOMLocationFilter: UIView { - - enum Distance: String { - case under1Km = "UNDER_1" - case under5Km = "UNDER_5" - case under10Km = "UNDER_10" - case under20Km = "UNDER_20" - case under50Km = "UNDER_50" - - var text: String { - switch self { - case .under1Km: - "~ 1km" - case .under5Km: - "1km ~ 5km" - case .under10Km: - "5km ~ 10km" - case .under20Km: - "10km ~ 20km" - case .under50Km: - "20km ~ 50km" - } - } - } - - static let height: CGFloat = 54 - - /// 델리게이트 - weak var delegate: SOMLocationFilterDelegate? - - /// 거리 이넘 정보 들어있는 배열 - let distances: [Distance] = [.under1Km, .under5Km, .under10Km, .under20Km, .under50Km] - - /// 이전에 선택된 필터 - var prevDistance: Distance = .under1Km - /// 현재 선택된 필터 - var selectedDistance: Distance = .under1Km - - /// 로케이션 필터 버튼 컬렉션 뷰 - let locationFilterCollectionView = UICollectionView( - frame: .zero, - collectionViewLayout: UICollectionViewFlowLayout().then { - $0.scrollDirection = .horizontal // 스크롤 방향을 가로로 설정 - } - ).then { - $0.backgroundColor = .clear - $0.register( - SOMLocationFilterCollectionViewCell.self, - forCellWithReuseIdentifier: String(describing: SOMLocationFilterCollectionViewCell.self) - ) - } - - // MARK: - init - override init(frame: CGRect) { - super.init(frame: frame) - initUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - initUI - private func initUI() { - self.backgroundColor = .clear - locationFilterCollectionView.showsHorizontalScrollIndicator = false - addSubviews() - initDelegate() - initConstraint() - } - - private func addSubviews() { - self.addSubview(locationFilterCollectionView) - } - - // MARK: - initDelegate - private func initDelegate() { - locationFilterCollectionView.dataSource = self - locationFilterCollectionView.delegate = self - } - - // MARK: - initConstraint - private func initConstraint() { - locationFilterCollectionView.snp.makeConstraints { - $0.leading.equalToSuperview() - $0.top.equalToSuperview() - $0.trailing.equalToSuperview() - $0.bottom.equalToSuperview() - } - } -} - -// MARK: - UICollectionView -extension SOMLocationFilter: - UICollectionViewDataSource, - UICollectionViewDelegate, - UICollectionViewDelegateFlowLayout { - - // MARK: - DataSource - func collectionView( - _ collectionView: UICollectionView, - numberOfItemsInSection section: Int - ) -> Int { - return distances.count - } - - func collectionView( - _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: - String(describing: SOMLocationFilterCollectionViewCell.self), - for: indexPath - ) as! SOMLocationFilterCollectionViewCell - - let distance = distances[indexPath.item] - let isSelected = distance == selectedDistance - cell.setData(distance: distance, isSelected: isSelected) - return cell - } - - // MARK: - Delegate - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - /// 새로 선택된 거리 필터 - let newDistance = distances[indexPath.item] - self.prevDistance = self.selectedDistance - self.selectedDistance = newDistance - self.locationFilterCollectionView.reloadData() - self.delegate?.filter(self, didSelectDistanceAt: newDistance) - } - - // MARK: - FlowLayout - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath - ) -> CGSize { - - let label = UILabel().then { - let distance = distances[indexPath.item] - $0.typography = .som.body3WithRegular - $0.text = distance.text - } - label.sizeToFit() - return CGSize(width: label.bounds.width + 32, height: label.bounds.height + 24) - } - - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetForSectionAt section: Int - ) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) - } - - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - minimumInteritemSpacingForSectionAt section: Int - ) -> CGFloat { - return 8 - } -} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMLocationFilter/SOMLocationFilterCollectionViewCell.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMLocationFilter/SOMLocationFilterCollectionViewCell.swift deleted file mode 100644 index 466a7700..00000000 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMLocationFilter/SOMLocationFilterCollectionViewCell.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SOMLocationFilterCollectionViewCell.swift -// SOOUM -// -// Created by JDeoks on 9/19/24. -// - -import UIKit - -import SnapKit -import Then - -class SOMLocationFilterCollectionViewCell: UICollectionViewCell { - - /// 거리 범위 텍스트 표시하는 라벨 - let label = UILabel().then { - $0.typography = .som.body3WithRegular - $0.textColor = .som.p300 - } - - override init(frame: CGRect) { - super.init(frame: frame) - initUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setData(distance: SOMLocationFilter.Distance, isSelected: Bool) { - label.text = distance.text - label.textColor = isSelected ? .som.p300 : .som.gray600 - contentView.layer.borderColor = isSelected - ? UIColor.som.p300.cgColor - : UIColor.som.gray300.cgColor - } - - // MARK: - initUI - private func initUI() { - contentView.backgroundColor = .white - contentView.layer.cornerRadius = contentView.frame.height / 2 - contentView.layer.borderWidth = 1 - contentView.layer.borderColor = UIColor.som.p300.cgColor - addSubviews() - initConstraint() - } - - private func addSubviews() { - self.addSubview(label) - } - - // MARK: - initConstraint - private func initConstraint() { - label.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.centerY.equalToSuperview() - } - } -} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageModel.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageModel.swift new file mode 100644 index 00000000..c457476b --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageModel.swift @@ -0,0 +1,31 @@ +// +// SOMPageModel.swift +// SOOUM +// +// Created by 오현식 on 10/2/25. +// + +import UIKit + + +class SOMPageModel { + + let data: NoticeInfo + let index: (current: Int, total: Int) + + init(data: NoticeInfo, index: (current: Int, total: Int)) { + self.data = data + self.index = index + } +} + +extension SOMPageModel: Hashable { + + static func == (lhs: SOMPageModel, rhs: SOMPageModel) -> Bool { + return lhs.data.id == rhs.data.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.data.id) + } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageView.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageView.swift new file mode 100644 index 00000000..a7c399c0 --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageView.swift @@ -0,0 +1,146 @@ +// +// SOMPageView.swift +// SOOUM +// +// Created by 오현식 on 10/2/25. +// + +import UIKit + +import SnapKit +import Then + +class SOMPageView: UICollectionViewCell { + + + // MARK: Views + + private let shadowbackgroundView = UIView().then { + $0.backgroundColor = .som.v2.white + $0.layer.cornerRadius = 16 + } + + private let indicatorContainer = UIStackView().then { + $0.axis = .horizontal + $0.alignment = .fill + $0.distribution = .equalSpacing + $0.spacing = 2 + } + + private let iconView = UIImageView() + + private let titleLabel = UILabel().then { + $0.textColor = .som.v2.gray400 + $0.typography = .som.v2.caption2.withAlignment(.left) + } + + private let messageLabel = UILabel().then { + $0.textColor = .som.v2.gray600 + $0.typography = .som.v2.subtitle3.withAlignment(.left) + } + + + // MARK: Variables + + private(set) var model: SOMPageModel? + + + // MARK: Initialize + + override init(frame: CGRect) { + super.init(frame: frame) + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Override func + + override func layoutSubviews() { + super.layoutSubviews() + + self.shadowbackgroundView.setShadow( + radius: 6, + color: UIColor(hex: "#ABBED11A").withAlphaComponent(0.1), + blur: 16, + offset: .init(width: 0, height: 6) + ) + } + + + // MARK: Private func + + private func setupConstraints() { + + self.contentView.addSubview(self.shadowbackgroundView) + self.shadowbackgroundView.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.leading.equalToSuperview().offset(16) + $0.trailing.equalToSuperview().offset(-16) + } + + self.shadowbackgroundView.addSubview(self.indicatorContainer) + self.indicatorContainer.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.trailing.equalToSuperview().offset(-16) + } + + let iconBackgroundView = UIView().then { + $0.backgroundColor = .som.v2.gray100 + $0.layer.cornerRadius = 8 + } + iconBackgroundView.addSubview(self.iconView) + self.iconView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.size.equalTo(20) + } + self.shadowbackgroundView.addSubview(iconBackgroundView) + iconBackgroundView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().offset(16) + $0.size.equalTo(28) + } + + let contentsContainer = UIStackView(arrangedSubviews: [self.titleLabel, self.messageLabel]).then { + $0.axis = .vertical + } + self.shadowbackgroundView.addSubview(contentsContainer) + contentsContainer.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalTo(iconBackgroundView.snp.trailing).offset(10) + $0.trailing.greaterThanOrEqualToSuperview().offset(-16) + } + } + + + // MARK: Public func + + func setModel(_ model: SOMPageModel) { + + self.model = model + + self.iconView.image = model.data.noticeType.image + self.iconView.tintColor = model.data.noticeType.tintColor + + self.titleLabel.text = model.data.noticeType.title + self.titleLabel.typography = .som.v2.caption2.withAlignment(.left) + self.messageLabel.text = model.data.message + self.messageLabel.typography = .som.v2.subtitle3.withAlignment(.left) + + self.indicatorContainer.arrangedSubviews.forEach { $0.removeFromSuperview() } + for index in 0.. + typealias Snapshot = NSDiffableDataSourceSnapshot + + private lazy var dataSource = DataSource(collectionView: self.collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in + + if case let .main(model) = item { + let cell: SOMPageView = collectionView.dequeueReusableCell( + withReuseIdentifier: "page", + for: indexPath + ) as! SOMPageView + cell.setModel(model) + + return cell + } else { + return nil + } + } + + weak var delegate: SOMPageViewsDelegate? + + + // MARK: Initialize + + override init(frame: CGRect) { + super.init(frame: frame) + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Private func + + private func setupConstraints() { + + self.addSubview(self.collectionView) + self.collectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + + // MARK: Public func + + func setModels(_ models: [SOMPageModel]) { + + let modelsToItem = models.map { Item.main($0) } + var snapshot = Snapshot() + snapshot.appendSections(Section.allCases) + snapshot.appendItems(modelsToItem, toSection: .main) + self.dataSource.apply(snapshot, animatingDifferences: false) + } +} + +extension SOMPageViews: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return } + + if case let .main(model) = item { + self.delegate?.pages(self, didTouch: model) + } + } + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { + let width: CGFloat = collectionView.bounds.width + let height: CGFloat = 71 + + return CGSize(width: width, height: height) + } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViewsDelegate.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViewsDelegate.swift new file mode 100644 index 00000000..c5930b86 --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMPageViews/SOMPageViewsDelegate.swift @@ -0,0 +1,18 @@ +// +// SOMPageViewsDelegate.swift +// SOOUM +// +// Created by 오현식 on 10/2/25. +// + +import Foundation + +protocol SOMPageViewsDelegate: AnyObject { + + func pages(_ tags: SOMPageViews, didTouch model: SOMPageModel) +} + +extension SOMPageViewsDelegate { + + func pages(_ tags: SOMPageViews, didTouch model: SOMPageModel) { } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift index ecbbc201..3c83b9ae 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMRefreshControl.swift @@ -10,22 +10,20 @@ import UIKit import SnapKit import Then +import Lottie class SOMRefreshControl: UIRefreshControl { - private let backgroundView = UIView().then { - $0.backgroundColor = .som.white - $0.layer.cornerRadius = 40 * 0.5 - } - private let imageView = UIImageView().then { - $0.image = .init(.icon(.outlined(.refresh))) - $0.tintColor = .som.black + // MARK: Views + + private let animationView = LottieAnimationView(name: "refrech_control_lottie").then { $0.contentMode = .scaleAspectFit + $0.loopMode = .loop } - // MARK: init + // MARK: Initialize convenience override init() { self.init(frame: .zero) @@ -47,25 +45,20 @@ class SOMRefreshControl: UIRefreshControl { // MARK: Override func + override func layoutSubviews() { + super.layoutSubviews() + + self.subviews.filter { $0 != self.animationView }.forEach { $0.removeFromSuperview() } + } + override func beginRefreshing() { super.beginRefreshing() - self.animation(true) + self.animationView.play() } override func endRefreshing() { super.endRefreshing() - self.animation(false) - } - - override func layoutSubviews() { - super.layoutSubviews() - - self.backgroundView.setShadow( - radius: 40 * 0.5, - color: UIColor.som.black.withAlphaComponent(0.25), - blur: 4, - offset: .init(width: 0, height: 4) - ) + self.animationView.stop() } @@ -77,31 +70,10 @@ class SOMRefreshControl: UIRefreshControl { self.tintColor = .clear - self.addSubview(self.backgroundView) - self.backgroundView.snp.makeConstraints { - $0.center.equalToSuperview() - $0.size.equalTo(40) - } - - self.backgroundView.addSubview(self.imageView) - self.imageView.snp.makeConstraints { + self.addSubview(self.animationView) + self.animationView.snp.makeConstraints { $0.center.equalToSuperview() - $0.size.equalTo(28) - } - } - - private func animation(_ isRefreshing: Bool) { - - if isRefreshing { - let rotate = CABasicAnimation(keyPath: "transform.rotation.z") - rotate.fromValue = 0 - rotate.toValue = NSNumber(value: Double.pi * -2.0) - rotate.duration = 1 - rotate.repeatCount = Float.infinity - rotate.timingFunction = CAMediaTimingFunction(name: .linear) - self.imageView.layer.add(rotate, forKey: "rotate") - } else { - self.imageView.layer.removeAnimation(forKey: "rotate") + $0.size.equalTo(44) } } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift new file mode 100644 index 00000000..5d987bee --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBar.swift @@ -0,0 +1,205 @@ +// +// SOMStickyTabBar.swift +// SOOUM +// +// Created by 오현식 on 12/21/24. +// + +import UIKit + +import SnapKit +import Then + + +class SOMStickyTabBar: UIView { + + enum Constants { + static let height: CGFloat = 56 + + static let selectedColor: UIColor = UIColor.som.v2.black + static let unSelectedColor: UIColor = UIColor.som.v2.gray400 + } + + + // MARK: Views + + private let tabBarItemContainer = UIStackView().then { + $0.axis = .horizontal + $0.alignment = .fill + $0.distribution = .equalSpacing + } + + private var tabBarItems: [UIView]? { + let items = self.tabBarItemContainer.arrangedSubviews + return items.isEmpty ? nil : items + } + + private let bottomSeperator = UIView().then { + $0.backgroundColor = .som.v2.gray200 + } + + + // MARK: Variables + + var inset: UIEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 16) { + didSet { + self.refreshConstraints() + } + } + + var spacing: CGFloat = 24 { + didSet { + self.refreshConstraints() + } + } + + var items: [String] = [] { + didSet { + if self.items.isEmpty == false { + self.setTabBarItems(self.items) + } + } + } + + // Set item width with text and typography + var itemWidths: [CGFloat] { + let itemWidths: [CGFloat] = self.items.enumerated().map { index, item in + let typography: Typography = .som.v2.title2 + /// 실제 텍스트 가로 길이 + return (item as NSString).size(withAttributes: [.font: typography.font]).width + } + + return itemWidths + } + + var itemFrames: [CGRect] { + var itemFrames: [CGRect] = [] + var currentX: CGFloat = self.inset.left + for itemWidth in self.itemWidths { + let itemFrame = CGRect(x: currentX, y: 0, width: itemWidth, height: self.bounds.height) + itemFrames.append(itemFrame) + currentX += itemWidth + self.spacing + } + + return itemFrames + } + + var previousIndex: Int = 0 + var selectedIndex: Int = 0 + + + // MARK: Constraints + + private var tabBarItemContainerTopConstraint: Constraint? + private var tabBarItemContainerBottomConstraint: Constraint? + private var tabBarItemContainerLeadingConstraint: Constraint? + private var tabBarItemContainerTrailingConstraint: Constraint? + + + // MARK: Delegate + + weak var delegate: SOMStickyTabBarDelegate? + + + // MARK: Initialize + + init() { + super.init(frame: .zero) + + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Override func + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + + guard let touch = touches.first else { return } + + let location = touch.location(in: self) + + for (index, frame) in self.itemFrames.enumerated() { + // 현재 선택한 좌표가 아이템의 내부이고 선택된 아이템이 아닐 때 + if frame.contains(location), self.selectedIndex != index { + // 선택할 수 있는 상태일 때 + if self.delegate?.tabBar(self, shouldSelectTabAt: index) ?? true { + self.didSelectTabBarItem(index) + } + } + } + + super.touchesEnded(touches, with: event) + } + + + // MARK: Private func + + private func setupConstraints() { + + self.addSubview(self.bottomSeperator) + self.bottomSeperator.snp.makeConstraints { + $0.bottom.leading.trailing.equalToSuperview() + $0.height.equalTo(1) + } + + self.addSubview(self.tabBarItemContainer) + self.tabBarItemContainer.snp.makeConstraints { + self.tabBarItemContainerTopConstraint = $0.top.equalToSuperview().offset(self.inset.top).constraint + self.tabBarItemContainerBottomConstraint = $0.bottom.equalToSuperview().offset(-self.inset.bottom).constraint + self.tabBarItemContainerLeadingConstraint = $0.leading.equalToSuperview().offset(self.inset.left).constraint + self.tabBarItemContainerTrailingConstraint = $0.trailing.lessThanOrEqualToSuperview().offset(-self.inset.right).constraint + } + } + + private func refreshConstraints() { + + self.tabBarItemContainer.spacing = self.spacing + + self.tabBarItemContainerTopConstraint?.deactivate() + self.tabBarItemContainerBottomConstraint?.deactivate() + self.tabBarItemContainerLeadingConstraint?.deactivate() + self.tabBarItemContainerTrailingConstraint?.deactivate() + self.tabBarItemContainer.snp.makeConstraints { + self.tabBarItemContainerTopConstraint = $0.top.equalToSuperview().offset(self.inset.top).constraint + self.tabBarItemContainerBottomConstraint = $0.bottom.equalToSuperview().offset(-self.inset.bottom).constraint + self.tabBarItemContainerLeadingConstraint = $0.leading.equalToSuperview().offset(self.inset.left).constraint + self.tabBarItemContainerTrailingConstraint = $0.trailing.lessThanOrEqualToSuperview().offset(-self.inset.right).constraint + } + } + + private func setTabBarItems(_ items: [String]) { + + items.enumerated().forEach { index, title in + + let item = SOMStickyTabBarItem(title: title) + item.updateState( + color: index == 0 ? Constants.selectedColor : Constants.unSelectedColor, + hasIndicator: index == 0 + ) + + self.tabBarItemContainer.addArrangedSubview(item) + } + } + + + // MARK: Public func + + func didSelectTabBarItem(_ index: Int) { + + self.tabBarItemContainer.arrangedSubviews.enumerated().forEach { + let selectedItem = $1 as? SOMStickyTabBarItem + selectedItem?.updateState( + color: $0 == index ? Constants.selectedColor : Constants.unSelectedColor, + hasIndicator: $0 == index + ) + } + + self.previousIndex = self.selectedIndex + self.selectedIndex = index + self.delegate?.tabBar(self, didSelectTabAt: index) + } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBarDelegate.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBarDelegate.swift new file mode 100644 index 00000000..cfbfe6ac --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBarDelegate.swift @@ -0,0 +1,19 @@ +// +// SOMStickyTabBarDelegate.swift +// SOOUM +// +// Created by 오현식 on 12/22/24. +// + +import Foundation + + +protocol SOMStickyTabBarDelegate: AnyObject { + func tabBar(_ tabBar: SOMStickyTabBar, shouldSelectTabAt index: Int) -> Bool + func tabBar(_ tabBar: SOMStickyTabBar, didSelectTabAt index: Int) +} + +extension SOMStickyTabBarDelegate { + func tabBar(_ tabBar: SOMStickyTabBar, shouldSelectTabAt index: Int) -> Bool { true } + func tabBar(_ tabBar: SOMStickyTabBar, didSelectTabAt index: Int) { } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBarItem.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBarItem.swift new file mode 100644 index 00000000..e0242a65 --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMStickyTabBar/SOMStickyTabBarItem.swift @@ -0,0 +1,75 @@ +// +// SOMStickyTabBarItem.swift +// SOOUM +// +// Created by 오현식 on 12/22/24. +// + +import UIKit + +import SnapKit +import Then + + +class SOMStickyTabBarItem: UIView { + + + // MARK: Views + + private let titleLabel = UILabel().then { + $0.textColor = .som.v2.gray400 + $0.typography = .som.v2.title2 + } + + private let selectedIndicator = UIView().then { + $0.backgroundColor = .som.v2.black + $0.isHidden = true + } + + + // MARK: Initialize + + convenience init(title: String) { + self.init(frame: .zero) + + self.titleLabel.text = title + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Private func + + private func setupConstraints() { + + self.addSubview(self.titleLabel) + self.titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.bottom.equalToSuperview().offset(-16) + $0.horizontalEdges.equalToSuperview() + } + + self.addSubview(self.selectedIndicator) + self.selectedIndicator.snp.makeConstraints { + $0.bottom.horizontalEdges.equalToSuperview() + $0.height.equalTo(2) + } + } + + + // MARK: Public func + + func updateState(color textColor: UIColor, hasIndicator: Bool) { + + self.titleLabel.textColor = textColor + self.selectedIndicator.isHidden = hasIndicator == false + } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift new file mode 100644 index 00000000..6b5e6045 --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBar.swift @@ -0,0 +1,204 @@ +// +// SOMSwipableTabBar.swift +// SOOUM +// +// Created by 오현식 on 9/23/25. +// + +import UIKit + +import SnapKit +import Then + + +class SOMSwipableTabBar: UIView { + + enum Constants { + static let height: CGFloat = 56 + + static let selectedTypo: Typography = Typography.som.v2.subtitle3 + static let unSelectedTypo: Typography = Typography.som.v2.body1 + + static let selectedColor: UIColor = UIColor.som.v2.gray600 + static let unSelectedColor: UIColor = UIColor.som.v2.gray400 + + static let selectedBackgroundColor: UIColor = UIColor.som.v2.gray100 + } + + + // MARK: Views + + private let tabBarItemContainer = UIStackView().then { + $0.axis = .horizontal + $0.alignment = .fill + $0.distribution = .equalSpacing + } + + private var tabBarItems: [UIView]? { + let items = self.tabBarItemContainer.arrangedSubviews + return items.isEmpty ? nil : items + } + + + // MARK: Variables + + var inset: UIEdgeInsets = .init(top: 9.5, left: 16, bottom: 9.5, right: 16) { + didSet { + self.refreshConstraints() + } + } + + var spacing: CGFloat = 0 { + didSet { + self.refreshConstraints() + } + } + + var items: [String] = [] { + didSet { + if self.items.isEmpty == false { + self.setTabBarItems(self.items) + } + } + } + + // Set item width with text and typography + var itemFrames: [CGRect] { + let itemWidths: [CGFloat] = self.items.enumerated().map { index, item in + let typography: Typography = self.selectedIndex == index ? Constants.selectedTypo : Constants.unSelectedTypo + /// 실제 텍스트 가로 길이 + 패딩 + return (item as NSString).size(withAttributes: [.font: typography.font]).width + 10 * 2 + } + + var itemFrames: [CGRect] = [] + var currentX: CGFloat = self.inset.left + for itemWidth in itemWidths { + let itemFrame = CGRect(x: currentX, y: 0, width: itemWidth, height: self.bounds.height) + itemFrames.append(itemFrame) + currentX += itemWidth + } + + return itemFrames + } + + var previousIndex: Int = 0 + var selectedIndex: Int = 0 + + + // MARK: Constraint + + private var tabBarItemContainerTopConstraint: Constraint? + private var tabBarItemContainerBottomConstraint: Constraint? + private var tabBarItemContainerLeadingConstraint: Constraint? + private var tabBarItemContainerTrailingConstraint: Constraint? + + + // MARK: Delegate + + weak var delegate: SOMSwipableTabBarDelegate? + + + // MARK: Initialize + + init() { + super.init(frame: .zero) + + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Override func + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + + guard let touch = touches.first else { return } + + let location = touch.location(in: self) + + for (index, frame) in self.itemFrames.enumerated() { + // 현재 선택한 좌표가 아이템의 내부이고 선택된 아이템이 아닐 때 + if frame.contains(location), self.selectedIndex != index { + // 선택할 수 있는 상태일 때 + if self.delegate?.tabBar(self, shouldSelectTabAt: index) ?? true { + self.didSelectTabBarItem(index) + } + } + } + + super.touchesEnded(touches, with: event) + } + + + // MARK: Private func + + private func setupConstraints() { + + self.snp.makeConstraints { + $0.height.equalTo(Constants.height) + } + + self.addSubview(self.tabBarItemContainer) + self.tabBarItemContainer.snp.makeConstraints { + self.tabBarItemContainerTopConstraint = $0.top.equalToSuperview().offset(self.inset.top).constraint + self.tabBarItemContainerBottomConstraint = $0.bottom.equalToSuperview().offset(-self.inset.bottom).constraint + self.tabBarItemContainerLeadingConstraint = $0.leading.equalToSuperview().offset(self.inset.left).constraint + self.tabBarItemContainerTrailingConstraint = $0.trailing.lessThanOrEqualToSuperview().offset(-self.inset.right).constraint + } + } + + private func refreshConstraints() { + + self.tabBarItemContainer.spacing = self.spacing + + self.tabBarItemContainerTopConstraint?.deactivate() + self.tabBarItemContainerBottomConstraint?.deactivate() + self.tabBarItemContainerLeadingConstraint?.deactivate() + self.tabBarItemContainerTrailingConstraint?.deactivate() + self.tabBarItemContainer.snp.makeConstraints { + self.tabBarItemContainerTopConstraint = $0.top.equalToSuperview().offset(self.inset.top).constraint + self.tabBarItemContainerBottomConstraint = $0.bottom.equalToSuperview().offset(-self.inset.bottom).constraint + self.tabBarItemContainerLeadingConstraint = $0.leading.equalToSuperview().offset(self.inset.left).constraint + self.tabBarItemContainerTrailingConstraint = $0.trailing.lessThanOrEqualToSuperview().offset(-self.inset.right).constraint + } + } + + private func setTabBarItems(_ items: [String]) { + + items.enumerated().forEach { index, title in + + let item = SOMSwipableTabBarItem(title: title) + item.updateState( + color: index == 0 ? Constants.selectedColor : Constants.unSelectedColor, + typo: index == 0 ? Constants.selectedTypo : Constants.unSelectedTypo, + backgroundColor: index == 0 ? Constants.selectedBackgroundColor : nil + ) + + self.tabBarItemContainer.addArrangedSubview(item) + } + } + + + // MARK: Public func + + func didSelectTabBarItem(_ index: Int, onlyUpdateApperance: Bool = false) { + + self.tabBarItemContainer.arrangedSubviews.enumerated().forEach { + let selectedItem = $1 as? SOMSwipableTabBarItem + selectedItem?.updateState( + color: index == $0 ? Constants.selectedColor : Constants.unSelectedColor, + typo: index == $0 ? Constants.selectedTypo : Constants.unSelectedTypo, + backgroundColor: index == $0 ? Constants.selectedBackgroundColor : nil + ) + } + + self.previousIndex = self.selectedIndex + self.selectedIndex = index + if onlyUpdateApperance == false { + self.delegate?.tabBar(self, didSelectTabAt: index) + } + } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarDelegate.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarDelegate.swift new file mode 100644 index 00000000..8068edc0 --- /dev/null +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarDelegate.swift @@ -0,0 +1,18 @@ +// +// SOMSwipableTabBarDelegate.swift +// SOOUM +// +// Created by 오현식 on 9/23/25. +// + +import Foundation + +protocol SOMSwipableTabBarDelegate: AnyObject { + func tabBar(_ tabBar: SOMSwipableTabBar, shouldSelectTabAt index: Int) -> Bool + func tabBar(_ tabBar: SOMSwipableTabBar, didSelectTabAt index: Int) +} + +extension SOMSwipableTabBarDelegate { + func tabBar(_ tabBar: SOMSwipableTabBar, shouldSelectTabAt index: Int) -> Bool { true } + func tabBar(_ tabBar: SOMSwipableTabBar, didSelectTabAt index: Int) { } +} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBarItem.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift similarity index 51% rename from SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBarItem.swift rename to SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift index e964802b..5bda374d 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBarItem.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipableTabBar/SOMSwipableTabBarItem.swift @@ -1,8 +1,8 @@ // -// SOMSwipeTabBarItem.swift +// SOMSwipableTabBarItem.swift // SOOUM // -// Created by 오현식 on 12/22/24. +// Created by 오현식 on 9/23/25. // import UIKit @@ -11,10 +11,16 @@ import SnapKit import Then -class SOMSwipeTabBarItem: UIView { +class SOMSwipableTabBarItem: UIView { + + + // MARK: Views private let titleLabel = UILabel() + + // MARK: Initialize + convenience init(title: String) { self.init(frame: .zero) @@ -31,23 +37,31 @@ class SOMSwipeTabBarItem: UIView { fatalError("init(coder:) has not been implemented") } + + // MARK: Private func + private func setupConstraints() { + self.layer.cornerRadius = 8 + self.clipsToBounds = true + self.addSubview(self.titleLabel) self.titleLabel.snp.makeConstraints { - $0.center.equalToSuperview() + $0.top.equalToSuperview().offset(8) + $0.bottom.equalToSuperview().offset(-8) + $0.leading.equalToSuperview().offset(10) + $0.trailing.equalToSuperview().offset(-10) } } func updateState( color textColor: UIColor, typo typography: Typography, - with duration: TimeInterval + backgroundColor: UIColor? = nil ) { - UIView.animate(withDuration: duration) { - self.titleLabel.textColor = textColor - self.titleLabel.typography = typography - } + self.titleLabel.textColor = textColor + self.titleLabel.typography = typography + self.backgroundColor = backgroundColor ?? .clear } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBar.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBar.swift deleted file mode 100644 index ef9832d2..00000000 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBar.swift +++ /dev/null @@ -1,240 +0,0 @@ -// -// SOMSwipeTabBar.swift -// SOOUM -// -// Created by 오현식 on 12/21/24. -// - -import UIKit - -import SnapKit -import Then - - -class SOMSwipeTabBar: UIView { - - enum Height { - static let mainHome: CGFloat = 40 - static let notification: CGFloat = 38 - } - - private let tabBarItemContainer = UIStackView().then { - $0.axis = .horizontal - $0.alignment = .fill - $0.distribution = .equalSpacing - } - - private var tabBarItems: [UIView]? { - let items = self.tabBarItemContainer.arrangedSubviews - return items.isEmpty ? nil : items - } - - private let bottomSeperator = UIView().then { - $0.backgroundColor = .som.gray200 - } - - private let selectedIndicator = UIView().then { - $0.backgroundColor = .som.p300 - } - - var inset: UIEdgeInsets = .init(top: 4, left: 12, bottom: 10, right: 0) { - didSet { - self.refreshConstraints() - } - } - - var spacing: CGFloat = 2 { - didSet { - self.refreshConstraints() - } - } - - var seperatorHeight: CGFloat = 1 { - didSet { - self.refreshConstraints() - } - } - - var seperatorColor: UIColor = .som.gray200 { - didSet { - self.bottomSeperator.backgroundColor = self.seperatorColor - } - } - - private var tabBarItemContainerTopConstraint: Constraint? - private var tabBarItemContainerBottomConstraint: Constraint? - private var tabBarItemContainerLeadingConstraint: Constraint? - - private var bottomSeperatorHeightConstraint: Constraint? - - private var selectedIndicatorLeadingConstraint: Constraint? - private var selectedIndicatorWidthConstraint: Constraint? - - private var itemAlignment: ItemAlignment - - private var defaultTypo: Typography - private var selectedTypo: Typography - - private var defaultColor: UIColor - private var selectedColor: UIColor - - var items: [String] = [] { - didSet { - if self.items.isEmpty == false { - self.setTabBarItems(self.items) - } - } - } - - var itemWidth: CGFloat { - let width = self.itemAlignment == .fill ? UIScreen.main.bounds.width / CGFloat(self.items.count) : 53 - return width - } - - weak var delegate: SOMSwipeTabBarDelegate? - - var previousIndex: Int = 0 - var selectedIndex: Int = 0 - - init(alignment: ItemAlignment) { - self.itemAlignment = alignment - self.selectedIndicator.isHidden = alignment == .left - - self.defaultTypo = alignment == .fill ? .som.body2WithRegular : .som.body2WithBold - self.selectedTypo = .som.body2WithBold - - self.defaultColor = alignment == .fill ? .som.gray400 : .som.gray500 - self.selectedColor = alignment == .fill ? .som.p300 : .som.black - - super.init(frame: .zero) - - self.setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - - self.addSubview(self.tabBarItemContainer) - self.tabBarItemContainer.snp.makeConstraints { - self.tabBarItemContainerTopConstraint = $0.top.equalToSuperview().offset(self.inset.top).constraint - self.tabBarItemContainerBottomConstraint = $0.bottom.equalToSuperview().offset(-self.inset.bottom).constraint - self.tabBarItemContainerLeadingConstraint = $0.leading.equalToSuperview().offset(self.inset.left).constraint - $0.trailing.lessThanOrEqualToSuperview() - } - - self.addSubview(self.bottomSeperator) - self.bottomSeperator.snp.makeConstraints { - $0.bottom.leading.trailing.equalToSuperview() - self.bottomSeperatorHeightConstraint = $0.height.equalTo(1).constraint - } - - self.addSubview(self.selectedIndicator) - self.selectedIndicator.snp.makeConstraints { - $0.bottom.equalToSuperview() - self.selectedIndicatorLeadingConstraint = $0.leading.equalToSuperview().constraint - $0.height.equalTo(1.6) - } - } - - private func refreshConstraints() { - - self.tabBarItemContainer.spacing = self.spacing - - self.tabBarItemContainerTopConstraint?.deactivate() - self.tabBarItemContainerBottomConstraint?.deactivate() - self.tabBarItemContainerLeadingConstraint?.deactivate() - self.tabBarItemContainer.snp.makeConstraints { - self.tabBarItemContainerTopConstraint = $0.top.equalToSuperview().offset(self.inset.top).constraint - self.tabBarItemContainerBottomConstraint = $0.bottom.equalToSuperview().offset(-self.inset.bottom).constraint - self.tabBarItemContainerLeadingConstraint = $0.leading.equalToSuperview().offset(self.inset.left).constraint - } - - self.bottomSeperatorHeightConstraint?.deactivate() - self.bottomSeperator.snp.makeConstraints { - self.bottomSeperatorHeightConstraint = $0.height.equalTo(self.seperatorHeight).constraint - } - } - - private func setTabBarItems(_ items: [String]) { - - items.enumerated().forEach { index, title in - - let item = SOMSwipeTabBarItem(title: title) - item.updateState( - color: index == 0 ? self.selectedColor : self.defaultColor, - typo: index == 0 ? self.selectedTypo : self.defaultTypo, - with: 0 - ) - - item.snp.makeConstraints { - $0.width.equalTo(self.itemWidth) - } - self.tabBarItemContainer.addArrangedSubview(item) - - if self.itemAlignment == .fill { - - self.selectedIndicatorWidthConstraint?.deactivate() - self.selectedIndicator.snp.makeConstraints { - self.selectedIndicatorWidthConstraint = $0.width.equalTo(self.itemWidth).constraint - } - } - } - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - super.touchesEnded(touches, with: event) - - guard let touchArea = touches.first?.location(in: self), - self.tabBarItemContainer.frame.contains(touchArea) else { return } - - let convertTouchAreaInContainer = convert(touchArea, to: self.tabBarItemContainer).x - let index = Int(floor(convertTouchAreaInContainer / self.itemWidth)) - - if self.selectedIndex != index, - self.delegate?.tabBar(self, shouldSelectTabAt: index) ?? true { - self.didSelectTabBarItem(index) - } - } - - func didSelectTabBarItem(_ index: Int, animated: Bool = true) { - - let animationDuration: TimeInterval = animated ? 0.25 : 0 - - self.tabBarItemContainer.arrangedSubviews.enumerated().forEach { - let selectedItem = $1 as? SOMSwipeTabBarItem - selectedItem?.updateState( - color: $0 == index ? self.selectedColor : self.defaultColor, - typo: $0 == index ? self.selectedTypo : self.defaultTypo, - with: $0 == index ? animationDuration : 0 - ) - } - - if self.itemAlignment == .fill { - let indicatorLeadingOffset: CGFloat = self.itemWidth * CGFloat(index) - - self.selectedIndicatorLeadingConstraint?.deactivate() - self.selectedIndicator.snp.makeConstraints { - self.selectedIndicatorLeadingConstraint = $0.leading.equalToSuperview().offset(indicatorLeadingOffset).constraint - } - - UIView.animate(withDuration: animationDuration) { - self.layoutIfNeeded() - } - } - - self.previousIndex = self.selectedIndex - self.selectedIndex = index - self.delegate?.tabBar(self, didSelectTabAt: index) - } -} - -extension SOMSwipeTabBar { - - enum ItemAlignment { - case left - case fill - } -} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBarDelegate.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBarDelegate.swift deleted file mode 100644 index d076011d..00000000 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMSwipeTabBar/SOMSwipeTabBarDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SOMSwipeTabBarDelegate.swift -// SOOUM -// -// Created by 오현식 on 12/22/24. -// - -import Foundation - - -protocol SOMSwipeTabBarDelegate: AnyObject { - func tabBar(_ tabBar: SOMSwipeTabBar, shouldSelectTabAt index: Int) -> Bool - func tabBar(_ tabBar: SOMSwipeTabBar, didSelectTabAt index: Int) -} diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBar.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBar.swift index 6208d98c..16893a17 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBar.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBar.swift @@ -17,54 +17,73 @@ protocol SOMTabBarDelegate: AnyObject { } class SOMTabBar: UIView { - - static let height: CGFloat = 58 + + + // MARK: Views private var tabBarItemContainer = UIStackView().then { $0.axis = .horizontal $0.distribution = .equalSpacing $0.alignment = .center + $0.spacing = 12 } private let tabBarBackgroundView = UIView().then { - $0.backgroundColor = .clear - $0.layer.cornerRadius = 58 * 0.5 + $0.backgroundColor = .som.v2.white + $0.layer.cornerRadius = 20 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.som.v2.gray200.cgColor $0.clipsToBounds = true } - private let blurView = UIVisualEffectView().then { - let blurEffect = UIBlurEffect(style: .regular) - $0.effect = blurEffect - $0.backgroundColor = UIColor.som.white.withAlphaComponent(0.7) - $0.alpha = 0.9 - } - var viewControllers: [UIViewController] = [] { didSet { guard self.viewControllers.isEmpty == false else { return } self.setTabBarItemConstraints() - self.setupConstraints() - self.didSelectTab(0) + self.didSelectTabBarItem(0) } } + + // MARK: Delegate + weak var delegate: SOMTabBarDelegate? - private let width: CGFloat = UIScreen.main.bounds.width - 20 * 2 - private var selectedIndex: Int = -1 - private var prevSelectedIndex: Int = -1 + // MARK: Variables - private var tabWidth: CGFloat { - self.width / CGFloat(self.numberOfItems) + var itemSpacing: CGFloat { + return (UIScreen.main.bounds.width - 16 * 2 - 77 * 4) / 3 + } + + var itemFrames: [CGRect] { + var itemFrames: [CGRect] = [] + var currentX: CGFloat = 16 + let itemWidth: CGFloat = 77 + for _ in 0.., with event: UIEvent?) { - self.tabBarBackgroundView.addSubview(self.blurView) - self.blurView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.width.equalTo(self.width) - $0.height.equalTo(58) - } + guard let touch = touches.first else { return } - self.tabBarBackgroundView.addSubview(self.tabBarItemContainer) - self.tabBarItemContainer.snp.makeConstraints { - $0.top.equalToSuperview().offset(4) - $0.bottom.equalToSuperview().offset(-4) - $0.leading.equalToSuperview().offset(4) - $0.trailing.equalToSuperview().offset(-4) + let location = touch.location(in: self) + + for (index, frame) in self.itemFrames.enumerated() { + // 현재 선택한 좌표가 아이템의 내부일 때 + if frame.contains(location) { + // 홈 탭을 다시 탭하면 scrollToTop + if index == 0, self.selectedIndex == index { + NotificationCenter.default.post(name: .scollingToTopWithAnimation, object: self) + } + // 이전에 선택된 아이템이 아닐 때 + if self.selectedIndex != index { + // 선택할 수 있는 상태일 때 + if self.delegate?.tabBar(self, shouldSelectTabAt: index) ?? true { + self.didSelectTabBarItem(index) + } + } + } } + super.touchesEnded(touches, with: event) + } + + + // MARK: Private func + + private func setupConstraints() { + self.addSubview(self.tabBarBackgroundView) self.tabBarBackgroundView.snp.makeConstraints { $0.edges.equalToSuperview() } + + self.tabBarBackgroundView.addSubview(self.tabBarItemContainer) + self.tabBarItemContainer.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.bottom.equalToSuperview().offset(-34) + $0.leading.equalToSuperview().offset(16) + $0.trailing.equalToSuperview().offset(-16) + } } private func setTabBarItemConstraints() { + self.tabBarItemContainer.spacing = self.itemSpacing + self.viewControllers.forEach { - let tabBarItem = SOMTabBarItem() - tabBarItem.title = $0.tabBarItem.title - tabBarItem.image = $0.tabBarItem.image + let tabBarItem = SOMTabBarItem(title: $0.tabBarItem.title, image: $0.tabBarItem.image) self.tabBarItemContainer.addArrangedSubview(tabBarItem) } } - func didSelectTab(_ index: Int ) { - - guard index != self.selectedIndex else { return } - self.delegate?.tabBar(self, didSelectTabAt: index) + + // MARK: Public func + + func didSelectTabBarItem(_ index: Int) { self.tabBarItemContainer.arrangedSubviews.enumerated().forEach { guard let tabView = $1 as? SOMTabBarItem else { return } @@ -120,15 +164,6 @@ class SOMTabBar: UIView { self.prevSelectedIndex = self.selectedIndex self.selectedIndex = index - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - - guard let touchArea = touches.first?.location(in: self).x else { return } - let index = Int(floor(touchArea / self.tabWidth)) - if self.delegate?.tabBar(self, shouldSelectTabAt: index) ?? false { - self.didSelectTab(index) - } + self.delegate?.tabBar(self, didSelectTabAt: index) } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift index c0112fcd..405630f0 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarController.swift @@ -67,10 +67,8 @@ class SOMTabBarController: UIViewController { self.view.addSubview(self.tabBar) self.view.bringSubviewToFront(self.tabBar) self.tabBar.snp.makeConstraints { - $0.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-4) - $0.leading.equalToSuperview().offset(20) - $0.trailing.equalToSuperview().offset(-20) - $0.height.equalTo(SOMTabBar.height) + $0.bottom.horizontalEdges.equalToSuperview() + $0.height.equalTo(88) } } @@ -89,7 +87,7 @@ class SOMTabBarController: UIViewController { func didSelectedIndex(_ index: Int) { - self.tabBar.didSelectTab(index) + self.tabBar.didSelectTabBarItem(index) } } diff --git a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift index 58db70e5..fc3e0d44 100644 --- a/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift +++ b/SOOUM/SOOUM/DesignSystem/Components/SOMTabBarController/SOMTabBarItem.swift @@ -13,30 +13,26 @@ import Then class SOMTabBarItem: UIView { - private let titleLabel = UILabel().then { - $0.textAlignment = .center - $0.typography = .som.caption - $0.textColor = .som.gray600 - } - var title: String? { - set { self.titleLabel.text = newValue } - get { self.titleLabel.text } - } + + // MARK: Views private let imageView = UIImageView().then { - $0.tintColor = .som.gray600 - } - var image: UIImage? { - didSet { self.imageView.image = self.image } + $0.tintColor = .som.gray400 } - private let backgroundView = UIView().then { - $0.backgroundColor = .clear - $0.layer.cornerRadius = 50 * 0.5 + private let titleLabel = UILabel().then { + $0.textColor = .som.v2.gray400 + $0.typography = .som.v2.caption2 } - convenience init() { + + // MARK: Initialize + + convenience init(title: String?, image: UIImage?) { self.init(frame: .zero) + + self.titleLabel.text = title + self.imageView.image = image } override init(frame: CGRect) { @@ -50,39 +46,33 @@ class SOMTabBarItem: UIView { private func setupConstraints() { - let container = UIStackView(arrangedSubviews: [self.imageView, self.titleLabel]).then { - $0.axis = .vertical - $0.alignment = .center - } - self.backgroundView.addSubview(container) - container.snp.makeConstraints { - $0.top.equalToSuperview().offset(8) - $0.bottom.equalToSuperview().offset(-8) - $0.leading.equalToSuperview().offset(12) - $0.trailing.equalToSuperview().offset(-12) + self.snp.makeConstraints { + $0.width.equalTo(77) + $0.height.equalTo(46) } + self.addSubview(self.imageView) self.imageView.snp.makeConstraints { + $0.top.centerX.equalToSuperview() $0.size.equalTo(24) } - self.addSubview(self.backgroundView) - self.backgroundView.snp.makeConstraints { - $0.edges.equalToSuperview() + self.addSubview(self.titleLabel) + self.titleLabel.snp.makeConstraints { + $0.top.equalTo(self.imageView.snp.bottom).offset(4) + $0.bottom.centerX.equalToSuperview() } } func tabBarItemSelected() { - self.titleLabel.textColor = .som.white - self.imageView.tintColor = .som.white - self.backgroundView.backgroundColor = .som.p300 + self.titleLabel.textColor = .som.v2.black + self.imageView.tintColor = .som.v2.black } func tabBarItemNotSelected() { - self.titleLabel.textColor = .som.gray600 - self.imageView.tintColor = .som.gray600 - self.backgroundView.backgroundColor = .clear + self.titleLabel.textColor = .som.v2.gray400 + self.imageView.tintColor = .som.v2.gray400 } } diff --git a/SOOUM/SOOUM/DesignSystem/Foundations/UIColor+SOOUM.swift b/SOOUM/SOOUM/DesignSystem/Foundations/UIColor+SOOUM.swift index 11e83082..ccd2bc69 100644 --- a/SOOUM/SOOUM/DesignSystem/Foundations/UIColor+SOOUM.swift +++ b/SOOUM/SOOUM/DesignSystem/Foundations/UIColor+SOOUM.swift @@ -76,7 +76,8 @@ extension V2Style where Base == UIColor { // Danger static let rLight = UIColor(hex: "#FFE1DF") - static let rMain = UIColor(hex: "#EE3A26") + static let rMain = UIColor(hex: "#E84B3D") + static let rDark = UIColor(hex: "#CC392C") // Dim static let dim = UIColor(r: 0, g: 0, b: 0, a: 0.6) diff --git a/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift b/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift index 5d028c5f..51109c77 100644 --- a/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift +++ b/SOOUM/SOOUM/DesignSystem/Foundations/UIImage+SOOUM.swift @@ -142,21 +142,29 @@ extension UIImage.SOOUMType { enum Filled: String { case bell + case bomb case camera + case card case danger case heart case home case image case info case location + case lock + case mail case message_circle case message_square + case notice + case official case settings case star case tag case time + case tool case trash case user + case users case write } @@ -204,10 +212,13 @@ extension UIImage.SOOUMType { case onboarding case onboarding_finish case check_square_light + case placeholder_home + case placeholder_notification case profile_large } enum V2LogoStyle: String { case logo_white + case logo_black } } diff --git a/SOOUM/SOOUM/Domain/Models/BaseCardInfo.swift b/SOOUM/SOOUM/Domain/Models/BaseCardInfo.swift new file mode 100644 index 00000000..e550b085 --- /dev/null +++ b/SOOUM/SOOUM/Domain/Models/BaseCardInfo.swift @@ -0,0 +1,81 @@ +// +// BaseCardInfo.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +struct BaseCardInfo: Hashable { + let id: String + let likeCnt: Int + let commentCnt: Int + let cardImgName: String + let cardImgURL: String + let cardContent: String + let font: Font + let distance: String? + let createdAt: Date + let storyExpirationTime: Date? + let isAdminCard: Bool +} + +extension BaseCardInfo { + /// 사용하는 폰트 + enum Font: String, Decodable { + case pretendard = "PRETENDARD" + case yoonwoo = "YOONWOO" + case ridi = "RIDI" + case kkookkkook = "KKOOKKKOOK" + } +} + +extension BaseCardInfo { + + static var defaultValue: BaseCardInfo = BaseCardInfo( + id: "", + likeCnt: 0, + commentCnt: 0, + cardImgName: "", + cardImgURL: "", + cardContent: "", + font: .pretendard, + distance: nil, + createdAt: Date(), + storyExpirationTime: nil, + isAdminCard: false + ) +} + +extension BaseCardInfo: Decodable { + + enum CodingKeys: String, CodingKey { + case id = "cardId" + case likeCnt + case commentCnt = "commentCardCnt" + case cardImgName + case cardImgURL = "cardImgUrl" + case cardContent + case font + case distance + case createdAt + case storyExpirationTime + case isAdminCard + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.likeCnt = try container.decode(Int.self, forKey: .likeCnt) + self.commentCnt = try container.decode(Int.self, forKey: .commentCnt) + self.cardImgName = try container.decode(String.self, forKey: .cardImgName) + self.cardImgURL = try container.decode(String.self, forKey: .cardImgURL) + self.cardContent = try container.decode(String.self, forKey: .cardContent) + self.font = try container.decode(BaseCardInfo.Font.self, forKey: .font) + self.distance = try container.decodeIfPresent(String.self, forKey: .distance) + self.createdAt = try container.decode(Date.self, forKey: .createdAt) + self.storyExpirationTime = try container.decodeIfPresent(Date.self, forKey: .storyExpirationTime) + self.isAdminCard = try container.decode(Bool.self, forKey: .isAdminCard) + } +} diff --git a/SOOUM/SOOUM/Domain/Models/CheckAvailable.swift b/SOOUM/SOOUM/Domain/Models/CheckAvailable.swift index 2776f6fd..0661fb17 100644 --- a/SOOUM/SOOUM/Domain/Models/CheckAvailable.swift +++ b/SOOUM/SOOUM/Domain/Models/CheckAvailable.swift @@ -13,13 +13,6 @@ struct CheckAvailable: Equatable { let banned: Bool let withdrawn: Bool let registered: Bool - - enum CodingKeys: String, CodingKey { - case rejoinAvailableAt - case banned - case withdrawn - case registered - } } extension CheckAvailable { @@ -34,6 +27,13 @@ extension CheckAvailable { extension CheckAvailable: Decodable { + enum CodingKeys: String, CodingKey { + case rejoinAvailableAt + case banned + case withdrawn + case registered + } + init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.rejoinAvailableAt = try container.decodeIfPresent(Date.self, forKey: .rejoinAvailableAt) diff --git a/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift b/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift index 56b71de1..62e2c4fa 100644 --- a/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift +++ b/SOOUM/SOOUM/Domain/Models/CommonNotificationInfo.swift @@ -7,17 +7,11 @@ import Foundation -struct CommonNotificationInfo: Equatable { +struct CommonNotificationInfo: Hashable, Equatable { let notificationId: String let notificationType: NotificationType let createTime: Date - - enum CodingKeys: CodingKey { - case notificationId - case notificationType - case createTime - } } extension CommonNotificationInfo { @@ -48,6 +42,12 @@ extension CommonNotificationInfo { extension CommonNotificationInfo.NotificationType: Decodable { } extension CommonNotificationInfo: Decodable { + enum CodingKeys: CodingKey { + case notificationId + case notificationType + case createTime + } + init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.notificationId = String(try container.decode(Int.self, forKey: .notificationId)) diff --git a/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift b/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift new file mode 100644 index 00000000..79a3e72d --- /dev/null +++ b/SOOUM/SOOUM/Domain/Models/CompositeNotificationInfo.swift @@ -0,0 +1,50 @@ +// +// CompositeNotificationInfo.swift +// SOOUM +// +// Created by 오현식 on 9/27/25. +// + +import Foundation + +enum CompositeNotificationInfo: Hashable, Equatable { + case `default`(NotificationInfoResponse) + case follow(FollowNotificationInfoResponse) + case deleted(DeletedNotificationInfoResponse) + case blocked(BlockedNotificationInfoResponse) +} + +extension CompositeNotificationInfo: Decodable { + + enum CodingKeys: String, CodingKey { + case notificationType + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let notificationType = try container.decode(CommonNotificationInfo.NotificationType.self, forKey: .notificationType) + + switch notificationType { + case .follow: + let notification = try FollowNotificationInfoResponse(from: decoder) + self = .follow(notification) + case .deleted: + let notification = try DeletedNotificationInfoResponse(from: decoder) + self = .deleted(notification) + case .blocked: + let notification = try BlockedNotificationInfoResponse(from: decoder) + self = .blocked(notification) + case .feedLike, .commentLike, .commentWrite: + let notification = try NotificationInfoResponse(from: decoder) + self = .default(notification) + // TODO: NOTICE, TRANSFER_SUCCESS 는 아직 정해지지 않음 + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unsupported notification type" + ) + ) + } + } +} diff --git a/SOOUM/SOOUM/Domain/Models/NoticeInfo.swift b/SOOUM/SOOUM/Domain/Models/NoticeInfo.swift new file mode 100644 index 00000000..0c607ba0 --- /dev/null +++ b/SOOUM/SOOUM/Domain/Models/NoticeInfo.swift @@ -0,0 +1,101 @@ +// +// NoticeInfo.swift +// SOOUM +// +// Created by 오현식 on 9/26/25. +// + +import UIKit + +struct NoticeInfo { + + let id: String + let noticeType: NoticeType + let message: String + let url: String + let createdAt: Date +} + +extension NoticeInfo { + + enum NoticeType: String, Decodable { + case announcement = "ANNOUNCEMENT" + case news = "NEWS" + case maintenance = "MAINTENANCE" + + var title: String { + switch self { + case .announcement: + return "서비스 안내" + case .news: + return "숨 새소식" + case .maintenance: + return "서비스 점검" + } + } + + var image: UIImage? { + switch self { + case .announcement: + return .init(.icon(.v2(.filled(.notice)))) + case .news: + return .init(.icon(.v2(.filled(.mail)))) + case .maintenance: + return .init(.icon(.v2(.filled(.tool)))) + } + } + + var tintColor: UIColor { + switch self { + case .announcement: + return .som.v2.rMain + case .news: + return .som.v2.pMain + case .maintenance: + return .som.v2.gray400 + } + } + } +} + +extension NoticeInfo { + + static var defaultValue: NoticeInfo = NoticeInfo( + id: "", + noticeType: .announcement, + message: "", + url: "", + createdAt: Date() + ) +} + +extension NoticeInfo: Hashable { + + static func == (lhs: NoticeInfo, rhs: NoticeInfo) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} + +extension NoticeInfo: Decodable { + + enum CodingKeys: String, CodingKey { + case id + case noticeType + case message = "title" + case url + case createdAt + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = String(try container.decode(Int64.self, forKey: .id)) + self.noticeType = try container.decode(NoticeType.self, forKey: .noticeType) + self.message = try container.decode(String.self, forKey: .message) + self.url = try container.decode(String.self, forKey: .url) + self.createdAt = try container.decode(Date.self, forKey: .createdAt) + } +} diff --git a/SOOUM/SOOUM/Domain/Models/SignUpRequestInfo.swift b/SOOUM/SOOUM/Domain/Models/SignUpRequestInfo.swift deleted file mode 100644 index d2272f9d..00000000 --- a/SOOUM/SOOUM/Domain/Models/SignUpRequestInfo.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// SignUpRequestInfo.swift -// SOOUM -// -// Created by 오현식 on 9/16/25. -// - -import Foundation - -struct MemberInfo: Equatable { - - let encryptedDeviceId: String - let deviceType: String - let fcmToken: String - let isNotificationAgreed: Bool -} - -extension MemberInfo { - - init(encryptedDeviceId: String, fcmToken: String, isNotificationAgreed: Bool) { - self.encryptedDeviceId = encryptedDeviceId - self.deviceType = "IOS" - self.fcmToken = fcmToken - self.isNotificationAgreed = isNotificationAgreed - } -} - -extension MemberInfo: Encodable { } - -struct Policy: Equatable { - - let agreedToTermsOfService: Bool - let agreedToLocationTerms: Bool - let agreedToPrivacyPolicy: Bool -} - -extension Policy: Encodable { } diff --git a/SOOUM/SOOUM/Domain/Models/Token.swift b/SOOUM/SOOUM/Domain/Models/Token.swift index 3c7bd006..97ead7f0 100644 --- a/SOOUM/SOOUM/Domain/Models/Token.swift +++ b/SOOUM/SOOUM/Domain/Models/Token.swift @@ -7,7 +7,7 @@ import Foundation -struct Token: Decodable { +struct Token: Equatable { var accessToken: String var refreshToken: String @@ -17,3 +17,5 @@ extension Token { static var defaultValue: Token = Token(accessToken: "", refreshToken: "") } + +extension Token: Decodable { } diff --git a/SOOUM/SOOUM/Domain/Models/Version.swift b/SOOUM/SOOUM/Domain/Models/Version.swift index 2114a221..48723d6e 100644 --- a/SOOUM/SOOUM/Domain/Models/Version.swift +++ b/SOOUM/SOOUM/Domain/Models/Version.swift @@ -7,7 +7,7 @@ import Foundation -struct Version { +struct Version: Equatable { let currentVersionStatus: Status } diff --git a/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift b/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift new file mode 100644 index 00000000..8b87af4c --- /dev/null +++ b/SOOUM/SOOUM/Domain/Repositories/CardRepository.swift @@ -0,0 +1,17 @@ +// +// CardRepository.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +import RxSwift + +protocol CardRepository { + + func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable + func popularCard(latitude: String?, longitude: String?) -> Observable + func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable +} diff --git a/SOOUM/SOOUM/Domain/Repositories/NotificationRepository.swift b/SOOUM/SOOUM/Domain/Repositories/NotificationRepository.swift index ef9ba71a..201c3664 100644 --- a/SOOUM/SOOUM/Domain/Repositories/NotificationRepository.swift +++ b/SOOUM/SOOUM/Domain/Repositories/NotificationRepository.swift @@ -11,13 +11,8 @@ import RxSwift protocol NotificationRepository { - func unreadNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> + func unreadNotifications(lastId: String?) -> Observable + func readNotifications(lastId: String?) -> Observable func requestRead(notificationId: String) -> Observable + func notices(lastId: String?) -> Observable } diff --git a/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift new file mode 100644 index 00000000..d133861d --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/CardUseCaseImpl.swift @@ -0,0 +1,34 @@ +// +// CardUseCaseImpl.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +import RxSwift + +class CardUseCaseImpl: CardUseCase { + + private let repository: CardRepository + + init(repository: CardRepository) { + self.repository = repository + } + + func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> { + + return self.repository.latestCard(lastId: lastId, latitude: latitude, longitude: longitude).map { $0.cardInfos } + } + + func popularCard(latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> { + + return self.repository.popularCard(latitude: latitude, longitude: longitude).map { $0.cardInfos } + } + + func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable<[BaseCardInfo]> { + + return self.repository.distanceCard(lastId: lastId, latitude: latitude, longitude: longitude, distanceFilter: distanceFilter).map { $0.cardInfos } + } +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift new file mode 100644 index 00000000..6305ba77 --- /dev/null +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/CardUseCase.swift @@ -0,0 +1,17 @@ +// +// CardUseCase.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import Foundation + +import RxSwift + +protocol CardUseCase { + + func latestCard(lastId: String?, latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> + func popularCard(latitude: String?, longitude: String?) -> Observable<[BaseCardInfo]> + func distanceCard(lastId: String?, latitude: String, longitude: String, distanceFilter: String) -> Observable<[BaseCardInfo]> +} diff --git a/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift b/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift index 25a030d6..976bda96 100644 --- a/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift +++ b/SOOUM/SOOUM/Domain/UseCases/Interfaces/NotificationUseCase.swift @@ -11,13 +11,8 @@ import RxSwift protocol NotificationUseCase { - func unreadNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func unreadNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> - func readNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> + func unreadNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> + func readNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> func requestRead(notificationId: String) -> Observable + func notices(lastId: String?) -> Observable<[NoticeInfo]> } diff --git a/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift b/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift index 6d9f818f..18ae4548 100644 --- a/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift +++ b/SOOUM/SOOUM/Domain/UseCases/NotificationUseCaseImpl.swift @@ -17,48 +17,23 @@ class NotificationUseCaseImpl: NotificationUseCase { self.repository = repository } - func unreadNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func unreadNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> { - return self.repository.unreadNotifications(lastId: lastId) + return self.repository.unreadNotifications(lastId: lastId).map { $0.notificationInfo } } - func unreadCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func readNotifications(lastId: String?) -> Observable<[CompositeNotificationInfo]> { - return self.repository.unreadCardNotifications(lastId: lastId) + return self.repository.readNotifications(lastId: lastId).map { $0.notificationInfo } } - func unreadFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.repository.unreadFollowNotifications(lastId: lastId) - } - - func unreadNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.repository.unreadNoticeNoticeNotifications(lastId: lastId) - } - - func readNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.repository.readNotifications(lastId: lastId) - } - - func readCardNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.repository.readCardNotifications(lastId: lastId) - } - - func readFollowNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { - - return self.repository.readFollowNotifications(lastId: lastId) - } - - func readNoticeNoticeNotifications(lastId: String?) -> Observable<[NotificationInfoResponse]> { + func requestRead(notificationId: String) -> Observable { - return self.repository.readNoticeNoticeNotifications(lastId: lastId) + return self.repository.requestRead(notificationId: notificationId).map { $0 == 200 } } - func requestRead(notificationId: String) -> Observable { + func notices(lastId: String?) -> Observable<[NoticeInfo]> { - return self.repository.requestRead(notificationId: notificationId).map { $0 == 200 } + return self.repository.notices(lastId: lastId).map { $0.noticeInfos } } } diff --git a/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift b/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift index cdbf134b..033c5463 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/Notification.swift @@ -14,4 +14,6 @@ extension Notification.Name { static let hidesBottomBarWhenPushedDidChange = Notification.Name("hidesBottomBarWhenPushedDidChange") /// Update location auth state static let changedLocationAuthorization = Notification.Name("changedLocationAuthorization") + /// Should scroll to top + static let scollingToTopWithAnimation = Notification.Name("scollingToTopWithAnimation") } diff --git a/SOOUM/SOOUM/Extensions/Cocoa/UIImgeView.swift b/SOOUM/SOOUM/Extensions/Cocoa/UIImgeView.swift index 967fa8cc..fc493bce 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/UIImgeView.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/UIImgeView.swift @@ -15,28 +15,15 @@ import RxSwift extension UIImageView { - static let placeholder: UIImage? = UIColor.som.gray400.toImage + static let placeholder: UIImage? = UIColor.som.v2.pMain.toImage - func setImage(strUrl: String?) { + func setImage(strUrl: String?, with key: String) { if let strUrl: String = strUrl, let url = URL(string: strUrl) { - // 캐싱된 이미지가 있다면, 먼저 사용 - ImageCache.default.retrieveImage(forKey: url.absoluteString) { result in - switch result { - case let .success(value): - if let image = value.image { - self.image = image - } else { - self.kf.setImage( - with: url, - placeholder: Self.placeholder, - options: [.transition(.fade(0.25))] - ) - } - case let .failure(error): - Log.error("Error download image failed with kingfisher: \(error.localizedDescription)") - } - } + /// ImageResource 객체를 생성하여 URL과 Cache Key를 연결 + let resource = KF.ImageResource(downloadURL: url, cacheKey: key) + /// Kingfisher에 Resource를 전달하고 모든 캐시/다운로드 로직 위임 + self.kf.setImage(with: resource) self.backgroundColor = .clear } else { diff --git a/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift b/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift index a8fb41f4..efb219a4 100644 --- a/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift +++ b/SOOUM/SOOUM/Extensions/Cocoa/UIRefreshControl.swift @@ -11,7 +11,8 @@ extension UIRefreshControl { func beginRefreshingFromTop() { if let scrollView: UIScrollView = superview as? UIScrollView { - let offset = CGPoint(x: 0, y: -self.frame.size.height) + /// refreshControl height + scrolled top inset + let offset = CGPoint(x: 0, y: -(self.frame.size.height + scrollView.adjustedContentInset.top)) scrollView.setContentOffset(offset, animated: true) } self.beginRefreshing() diff --git a/SOOUM/SOOUM/Extensions/Foundation/Date.swift b/SOOUM/SOOUM/Extensions/Foundation/Date.swift index d7da04ea..fcb39ee4 100644 --- a/SOOUM/SOOUM/Extensions/Foundation/Date.swift +++ b/SOOUM/SOOUM/Extensions/Foundation/Date.swift @@ -42,28 +42,36 @@ extension Date { let hours: Int = .init(time % (24 * 60 * 60)) / (60 * 60) let minutes: Int = .init(time % (60 * 60)) / 60 - if days > 364 { - return "\(days / 365)년전".trimmingCharacters(in: .whitespaces) + if days > 368 { + return "\(days / 368)년전".trimmingCharacters(in: .whitespaces) } - if days > 0 && days < 365 { - return "\(days)일전".trimmingCharacters(in: .whitespaces) + if days > 29 && days < 369 { + return "\(days / 30)개월 전".trimmingCharacters(in: .whitespaces) + } + + if days > 6 && days < 30 { + return "\(days / 7)주 전".trimmingCharacters(in: .whitespaces) + } + + if days > 0 && days < 6 { + return "\(days)일 전".trimmingCharacters(in: .whitespaces) } if hours > 0 && hours < 24 { - return "\(hours)시간전".trimmingCharacters(in: .whitespaces) + return "\(hours)시간 전".trimmingCharacters(in: .whitespaces) } - if minutes > 9 && minutes < 60 { - return "\(minutes / 10)0분전".trimmingCharacters(in: .whitespaces) + if minutes > 10 && minutes < 60 { + return "\(minutes / 10)0분 전".trimmingCharacters(in: .whitespaces) } - if minutes > 4 && minutes < 10 { - return "10분전".trimmingCharacters(in: .whitespaces) + if minutes > 0 && minutes < 11 { + return "\(minutes)분 전".trimmingCharacters(in: .whitespaces) } - if minutes < 5 { - return "조금전".trimmingCharacters(in: .whitespaces) + if minutes < 1 { + return "방금 전".trimmingCharacters(in: .whitespaces) } return "" @@ -97,6 +105,10 @@ extension Date { var announcementFormatted: String { return self.toString("yyyy. MM. dd") } + + var noticeFormatted: String { + return self.toString("M월 d일") + } } extension Locale { diff --git a/SOOUM/SOOUM/Extensions/Foundation/Log/Log+Extract.swift b/SOOUM/SOOUM/Extensions/Foundation/Log/Log+Extract.swift index c833822b..94900c77 100644 --- a/SOOUM/SOOUM/Extensions/Foundation/Log/Log+Extract.swift +++ b/SOOUM/SOOUM/Extensions/Foundation/Log/Log+Extract.swift @@ -21,21 +21,20 @@ extension Log { let filePaths: [String] = fileLogger.logFileManager.sortedLogFilePaths - if filePaths.isEmpty { - + if let latesFilePath = filePaths.last { + let fileUrls: [URL] = [URL(fileURLWithPath: latesFilePath)] + let viewController = UIActivityViewController( + activityItems: fileUrls, + applicationActivities: nil + ) + observer(.success(viewController)) + } else { let error = NSError( domain: "\(identifier):Log", code: -999, userInfo: [NSLocalizedDescriptionKey: "기록된 로그가 없습니다."] ) observer(.failure(error)) - } else { - let fileUrls: [URL] = filePaths.map { .init(fileURLWithPath: $0) } - let viewController = UIActivityViewController( - activityItems: fileUrls, - applicationActivities: nil - ) - observer(.success(viewController)) } } else { diff --git a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift index ae7c7120..95070848 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewController.swift @@ -27,10 +27,16 @@ class LaunchScreenViewController: BaseNavigationViewController, View { static let updateActionTitle: String = "새로워진 숨 사용하기" } + + // MARK: Views + let imageView = UIImageView(image: .init(.logo(.v2(.logo_white)))).then { $0.contentMode = .scaleAspectFit } + + // MARK: Override func + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } @@ -44,13 +50,15 @@ class LaunchScreenViewController: BaseNavigationViewController, View { self.view.addSubview(self.imageView) self.imageView.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.centerY.equalTo(self.view.safeAreaLayoutGuide.snp.centerY) + $0.center.equalToSuperview() $0.width.equalTo(200) $0.height.equalTo(33) } } + + // MARK: ReactorKit - bind + func bind(reactor: LaunchScreenViewReactor) { self.rx.viewDidLoad @@ -100,12 +108,12 @@ class LaunchScreenViewController: BaseNavigationViewController, View { isRegistered .filter { $0 == true } .subscribe(with: self) { object, _ in - // let viewController = MainTabBarController() - // viewController.reactor = reactor.reactorForMainTabBar() - // let navigationController = UINavigationController( - // rootViewController: viewController - // ) - // object.view.window?.rootViewController = navigationController + let viewController = MainTabBarController() + viewController.reactor = reactor.reactorForMainTabBar() + let navigationController = UINavigationController( + rootViewController: viewController + ) + object.view.window?.rootViewController = navigationController } .disposed(by: self.disposeBag) // 로그인 실패 시 온보딩 화면으로 전환 diff --git a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift index c0cd8e4e..f8f6c6b5 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Launch/LaunchScreenViewReactor.swift @@ -156,7 +156,7 @@ extension LaunchScreenViewReactor { OnboardingViewReactor(dependencies: self.dependencies) } - // func reactorForMainTabBar() -> MainTabBarReactor { - // MainTabBarReactor(provider: self.provider, pushInfo: self.pushInfo) - // } + func reactorForMainTabBar() -> MainTabBarReactor { + MainTabBarReactor(dependencies: self.dependencies) + } } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift index a7b1e521..546e36d9 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewController.swift @@ -81,49 +81,20 @@ class OnboardingCompletedViewController: BaseNavigationViewController, View { } } + + // MARK: ReactorKit - bind + func bind(reactor: OnboardingCompletedViewReactor) { // Action self.confirmButton.rx.throttleTap .subscribe(with: self) { object, _ in - - // TODO: 임시, 토큰 정보 삭제 후 계정 삭제 - let withdrawAction = SOMDialogAction( - title: "계정 삭제하기", - style: .primary, - action: { - - UIApplication.topViewController?.dismiss(animated: true) { - reactor.action.onNext(.withdraw) - } - } + let viewController = MainTabBarController() + viewController.reactor = reactor.reactorForMainTabBar() + let navigationController = UINavigationController( + rootViewController: viewController ) - - SOMDialogViewController.show( - title: "온보딩/회원가입 플로우 완료", - message: "생성된 계정 정보를 삭제하고 다시 스플레쉬 화면부터 시작합니다.", - textAlignment: .left, - actions: [withdrawAction] - ) - - // let viewController = MainTabBarController() - // viewController.reactor = reactor.reactorForMainTabBar() - // let navigationController = UINavigationController( - // rootViewController: viewController - // ) - // // 제스처 뒤로가기를 위한 델리게이트 설정 - // navigationController.interactivePopGestureRecognizer?.delegate = self - // object.view.window?.rootViewController = navigationController - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.isSuccess) - .distinctUntilChanged() - .filter { $0 } - .subscribe(with: self) { object, _ in - let viewController = LaunchScreenViewController() - viewController.reactor = reactor.reaactorForLaunch() - object.view.window?.rootViewController = viewController + object.view.window?.rootViewController = navigationController } .disposed(by: self.disposeBag) } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewReactor.swift index e3700b7b..5a855c06 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/Completed/OnboardingCompletedViewReactor.swift @@ -11,63 +11,25 @@ import Alamofire class OnboardingCompletedViewReactor: Reactor { - // typealias Action = NoAction - // typealias Mutation = NoMutation + typealias Action = NoAction + typealias Mutation = NoMutation - // struct State { } - // var initialState: State { .init() } - - enum Action { - case withdraw - } - - enum Mutation { - case withdraw(Bool) - } - - struct State { - var isSuccess: Bool - } - - var initialState: State = State(isSuccess: false) + struct State { } + var initialState: State { .init() } private let dependencies: AppDIContainerable - init(dependencies: AppDIContainerable) { self.dependencies = dependencies } - - func mutate(action: Action) -> Observable { - switch action { - case .withdraw: - - let managers = self.dependencies.rootContainer.resolve(ManagerProviderType.self) - return managers.networkManager.perform(Empty.self, request: AuthRequest.withdraw(token: managers.authManager.authInfo.token)) - .map { _ in true } - .map(Mutation.withdraw) - } - } - - - func reduce(state: State, mutation: Mutation) -> State { - var state = state - switch mutation { - case .withdraw(let isSuccess): - state.isSuccess = isSuccess - self.dependencies.rootContainer.resolve(ManagerProviderType.self).authManager.initializeAuthInfo() - } - return state - } } extension OnboardingCompletedViewReactor { - // TODO: 임시, 계정 삭제 후 런치 화면으로 이동 - func reaactorForLaunch() -> LaunchScreenViewReactor { - LaunchScreenViewReactor(dependencies: self.dependencies) + func reactorForNotification() -> NotificationViewReactor { + NotificationViewReactor(dependencies: self.dependencies) } func reactorForMainTabBar() -> MainTabBarReactor { - MainTabBarReactor(provider: self.dependencies.rootContainer.resolve(ManagerProviderType.self)) + MainTabBarReactor(dependencies: self.dependencies) } } diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift index cb5997c2..8006531e 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/NicknameSetting/OnboardingNicknameSettingViewController.swift @@ -53,7 +53,7 @@ class OnboardingNicknameSettingViewController: BaseNavigationViewController, Vie self.view.addSubview(self.guideMessageView) self.guideMessageView.snp.makeConstraints { - $0.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(16) + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(16) $0.leading.equalToSuperview().offset(16) $0.trailing.equalToSuperview().offset(-16) } @@ -66,7 +66,7 @@ class OnboardingNicknameSettingViewController: BaseNavigationViewController, Vie self.view.addSubview(self.nextButton) self.nextButton.snp.makeConstraints { - $0.bottom.equalTo(view.safeAreaLayoutGuide) + $0.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) $0.leading.equalToSuperview().offset(16) $0.trailing.equalToSuperview().offset(-16) $0.height.equalTo(56) diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift index bdd4a0d8..81bacb70 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewController.swift @@ -57,6 +57,8 @@ class OnboardingProfileImageSettingViewController: BaseNavigationViewController, // TODO: 임시, backgroundColor 넣어서 이미지 빈 곳 채움 $0.backgroundColor = .som.v2.gray300 $0.layer.cornerRadius = 120 * 0.5 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.som.v2.gray300.cgColor $0.clipsToBounds = true } private let cameraButton = SOMButton().then { diff --git a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift index fca2b961..0c00654f 100644 --- a/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Intro/Onboarding/ProfileImageSetting/OnboardingProfileImageSettingViewReactor.swift @@ -136,8 +136,8 @@ extension OnboardingProfileImageSettingViewReactor { .just(.updateImageInfo(nil, nil)), .just(.updateIsSignUp(false)), .just(.updateIsLoading(false)), - // 부적절한 이미지 업로드 에러 코드 == 402 - .just(.updateErrors(nsError.code == 402)) + // 부적절한 이미지 업로드 에러 코드 == 422 + .just(.updateErrors(nsError.code == 422)) ]) return endProcessing diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift new file mode 100644 index 00000000..74c940aa --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomePlaceholderViewCell.swift @@ -0,0 +1,71 @@ +// +// HomePlaceholderViewCell.swift +// SOOUM +// +// Created by 오현식 on 12/30/24. +// + +import UIKit + +import SnapKit +import Then + + +class HomePlaceholderViewCell: UITableViewCell { + + enum Text { + static let message: String = "아직 작성된 글이 없어요\n하고 싶은 이야기를 카드로 남겨보세요" + } + + static let cellIdentifier = String(reflecting: HomePlaceholderViewCell.self) + + + // MARK: Views + + private let placeholderImageView = UIImageView().then { + $0.image = .init(.image(.v2(.placeholder_home))) + } + + private let placeholderMessageLabel = UILabel().then { + $0.text = Text.message + $0.textColor = .som.v2.gray400 + $0.typography = .som.v2.body1 + $0.textAlignment = .center + } + + + // MARK: Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.selectionStyle = .none + self.backgroundColor = .clear + self.isUserInteractionEnabled = false + + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Private func + + private func setupConstraints() { + + self.contentView.addSubview(self.placeholderImageView) + self.placeholderImageView.snp.makeConstraints { + let offset = UIScreen.main.bounds.height * 0.2 + $0.top.equalToSuperview().offset(offset) + $0.centerX.equalToSuperview() + } + + self.contentView.addSubview(self.placeholderMessageLabel) + self.placeholderMessageLabel.snp.makeConstraints { + $0.top.equalTo(self.placeholderImageView.snp.bottom).offset(20) + $0.centerX.equalToSuperview() + } + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Cells/MainHomeViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomeViewCell.swift similarity index 64% rename from SOOUM/SOOUM/Presentations/Main/Home/Cells/MainHomeViewCell.swift rename to SOOUM/SOOUM/Presentations/Main/Home/Cells/HomeViewCell.swift index ed949940..d52f150b 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Cells/MainHomeViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Cells/HomeViewCell.swift @@ -1,5 +1,5 @@ // -// MainHomeViewCell.swift +// HomeViewCell.swift // SOOUM // // Created by 오현식 on 10/3/24. @@ -11,10 +11,18 @@ import SnapKit import Then -class MainHomeViewCell: UITableViewCell { +class HomeViewCell: UITableViewCell { + + static let cellIdentifier = String(reflecting: HomeViewCell.self) + + + // MARK: Views let cardView = SOMCard() + + // MARK: Initialize + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -28,33 +36,37 @@ class MainHomeViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } + + // MARK: Override func + override func prepareForReuse() { super.prepareForReuse() self.cardView.prepareForReuse() } + + // MARK: Private func + private func setupConstraints() { self.contentView.addSubview(self.cardView) self.cardView.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.bottom.equalToSuperview() - $0.leading.equalToSuperview().offset(20) - $0.trailing.equalToSuperview().offset(-20) + $0.top.equalToSuperview() + $0.bottom.equalToSuperview().offset(-10) + $0.leading.equalToSuperview().offset(16) + $0.trailing.equalToSuperview().offset(-16) } } - func setModel(_ model: SOMCardModel) { + + // MARK: Public info + + func bind(_ model: BaseCardInfo) { self.cardView.setModel(model: model) } func setData(tagCard: TagDetailCardResponse.TagFeedCard) { self.cardView.setData(tagCard: tagCard) } - - /// 컨텐츠 모드에 따라 정보 스택뷰 순서 변경 - func changeOrderInCardContentStack(_ selectedIndex: Int) { - self.cardView.changeOrderInCardContentStack(selectedIndex) - } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Cells/PlaceholderViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Cells/PlaceholderViewCell.swift deleted file mode 100644 index 1cddda8f..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Cells/PlaceholderViewCell.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// PlaceholderViewCell.swift -// SOOUM -// -// Created by 오현식 on 12/30/24. -// - -import UIKit - -import SnapKit -import Then - - -class PlaceholderViewCell: UITableViewCell { - - enum Text { - static let title: String = "아직 등록된 카드가 없어요" - static let firstSubTitle: String = "사소하지만 말 못 한 이야기를" - static let secondSubTitle: String = "카드로 만들어 볼까요?" - } - - - // MARK: Views - - private let placeholderTitleLabel = UILabel().then { - $0.text = Text.title - $0.textColor = .som.black - $0.textAlignment = .center - $0.typography = .som.body1WithBold - } - - private let placeholderFirstSubTitleLabel = UILabel().then { - $0.text = Text.firstSubTitle - $0.textColor = .som.gray500 - $0.textAlignment = .center - $0.typography = .som.body2WithBold - } - - private let placeholderSecondSubTitleLabel = UILabel().then { - $0.text = Text.secondSubTitle - $0.textColor = .som.gray500 - $0.textAlignment = .center - $0.typography = .som.body2WithBold - } - - - // MARK: Initalization - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - self.selectionStyle = .none - self.backgroundColor = .clear - self.isUserInteractionEnabled = false - - self.setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - // MARK: Private func - - private func setupConstraints() { - - self.contentView.addSubview(self.placeholderTitleLabel) - self.placeholderTitleLabel.snp.makeConstraints { - let offset = UIScreen.main.bounds.height * 0.2 - $0.top.equalToSuperview().offset(offset) - $0.centerX.equalToSuperview() - } - - self.contentView.addSubview(self.placeholderFirstSubTitleLabel) - self.placeholderFirstSubTitleLabel.snp.makeConstraints { - $0.top.equalTo(self.placeholderTitleLabel.snp.bottom).offset(14) - $0.centerX.equalToSuperview() - } - - self.contentView.addSubview(self.placeholderSecondSubTitleLabel) - self.placeholderSecondSubTitleLabel.snp.makeConstraints { - $0.top.equalTo(self.placeholderFirstSubTitleLabel.snp.bottom) - $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 2ba393e9..bdf27653 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewCell.swift @@ -94,7 +94,7 @@ class DetailViewCell: UICollectionViewCell { didSet { if let prevCard = self.prevCard { self.isPrevCardExist = true - self.prevCardBackgroundImageView.setImage(strUrl: prevCard.previousCardImgLink?.url ?? "") + self.prevCardBackgroundImageView.setImage(strUrl: prevCard.previousCardImgLink?.url ?? "", with: "") } else { self.isPrevCardExist = false } @@ -112,7 +112,7 @@ class DetailViewCell: UICollectionViewCell { var member: Member = .init() { didSet { if let strUrl = self.member.profileImgUrl?.url { - self.memberImageView.setImage(strUrl: strUrl) + self.memberImageView.setImage(strUrl: strUrl, with: "") } else { self.memberImageView.image = .init(.image(.defaultStyle(.sooumLogo))) } @@ -233,9 +233,8 @@ class DetailViewCell: UICollectionViewCell { } } - func setDatas(_ model: SOMCardModel, tags: [SOMTagModel]) { + func setDatas(_ model: BaseCardInfo, tags: [SOMTagModel]) { self.cardView.setModel(model: model) - self.cardView.removeLikeAndCommentInStack() self.tags.setModels(tags) self.tags.snp.updateConstraints { $0.height.equalTo(tags.isEmpty ? 40 : 59) diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooter.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooter.swift index 54950736..d6284a92 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooter.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooter.swift @@ -157,8 +157,8 @@ extension DetailViewFooter: UICollectionViewDataSource { as! DetailViewFooterCell let commentCard = self.commentCards[indexPath.row] - let model: SOMCardModel = .init(data: commentCard) - cell.setModel(model) + // let model: SOMCardModel = .init(data: commentCard) + // cell.setModel(model) return cell } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift index 1844c93b..ec81de16 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/Cells/DetailViewFooterCell.swift @@ -37,7 +37,7 @@ class DetailViewFooterCell: UICollectionViewCell { } } - func setModel(_ model: SOMCardModel) { + func setModel(_ model: BaseCardInfo) { self.cardView.setModel(model: model) } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift index b1c1d598..80846992 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewController.swift @@ -267,11 +267,12 @@ class DetailViewController: BaseNavigationViewController, View { object.collectionView.reloadData() } case .push: - let notificationTabBarController = NotificationTabBarController() - notificationTabBarController.reactor = reactor.reactorForNoti() - - object.navigationPush(notificationTabBarController, animated: false) - object.navigationController?.viewControllers.removeAll(where: { $0.isKind(of: DetailViewController.self) }) + 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) @@ -315,12 +316,12 @@ extension DetailViewController: UICollectionViewDataSource { isLiked: self.detailCard.isLiked, isCommentWritten: self.detailCard.isCommentWritten ) - let model: SOMCardModel = .init(data: card) + // let model: SOMCardModel = .init(data: card) let tags: [SOMTagModel] = self.detailCard.tags.map { SOMTagModel(id: $0.id, originalText: $0.content, isSelectable: true, isRemovable: false) } - cell.setDatas(model, tags: tags) + // cell.setDatas(model, tags: tags) cell.tags.delegate = self cell.isOwnCard = self.detailCard.isOwnCard cell.isPrevCardDelete = self.detailCard.isPreviousCardDelete diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift index a4397b41..fd36ab79 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Detail/DetailViewReactor.swift @@ -222,13 +222,13 @@ class DetailViewReactor: Reactor { extension DetailViewReactor { - func reactorForMainTabBar() -> MainTabBarReactor { - MainTabBarReactor(provider: self.provider) - } + // func reactorForMainTabBar() -> MainTabBarReactor { + // MainTabBarReactor(provider: self.provider) + // } - func reactorForMainHome() -> MainHomeTabBarReactor { - MainHomeTabBarReactor(provider: self.provider) - } + // func reactorForMainHome() -> MainHomeTabBarReactor { + // MainHomeTabBarReactor(provider: self.provider) + // } func reactorForPush(_ selectedId: String) -> DetailViewReactor { DetailViewReactor(provider: self.provider, selectedId) @@ -258,9 +258,9 @@ extension DetailViewReactor { ProfileViewReactor(provider: self.provider, type: type, memberId: memberId) } - func reactorForNoti() -> NotificationTabBarReactor { - NotificationTabBarReactor(provider: self.provider) - } + // func reactorForNoti() -> NotificationTabBarReactor { + // NotificationTabBarReactor(provider: self.provider) + // } } extension DetailViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Distance/MainHomeDistanceViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Distance/MainHomeDistanceViewController.swift deleted file mode 100644 index bf93886b..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Distance/MainHomeDistanceViewController.swift +++ /dev/null @@ -1,314 +0,0 @@ -// -// MainHomeDistanceViewController.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import UIKit - -import Kingfisher -import SnapKit -import Then - -import ReactorKit -import RxCocoa -import RxSwift - - -class MainHomeDistanceViewController: BaseViewController, View { - - - // MARK: Views - - private lazy var tableView = UITableView(frame: .zero, style: .plain).then { - $0.backgroundColor = .clear - $0.indicatorStyle = .black - $0.separatorStyle = .none - - $0.contentInset.top = SOMSwipeTabBar.Height.mainHome + SOMLocationFilter.height - - $0.isHidden = true - - $0.register(MainHomeViewCell.self, forCellReuseIdentifier: "cell") - $0.register(PlaceholderViewCell.self, forCellReuseIdentifier: "placeholder") - - $0.refreshControl = SOMRefreshControl() - - $0.dataSource = self - $0.prefetchDataSource = self - - $0.delegate = self - } - - private let moveTopButton = MoveTopButtonView().then { - $0.isHidden = true - } - - - // MARK: Variables - - // tableView 정보 - private var currentOffset: CGFloat = 0 - private var isRefreshEnabled: Bool = true - private var isLoadingMore: Bool = false - - private let cellHeight: CGFloat = { - let width: CGFloat = (UIScreen.main.bounds.width - 20 * 2) * 0.9 - return width + 10 /// 가로 + top inset - }() - - - // MARK: Variables + Rx - - let hidesHeaderContainer = PublishRelay() - let willPushCardId = PublishRelay() - - - // MARK: Override func - - override func setupConstraints() { - super.setupConstraints() - self.view.addSubview(self.tableView) - self.tableView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - self.view.addSubview(self.moveTopButton) - self.view.bringSubviewToFront(self.moveTopButton) - self.moveTopButton.snp.makeConstraints { - let bottomOffset: CGFloat = 24 + 60 + 4 + 20 - $0.bottom.equalTo(self.tableView.snp.bottom).offset(-bottomOffset) - $0.centerX.equalToSuperview() - $0.height.equalTo(MoveTopButtonView.height) - } - } - - override func bind() { - super.bind() - - // tableView 상단 이동 - self.moveTopButton.backgroundButton.rx.throttleTap(.seconds(3)) - .subscribe(with: self) { object, _ in - let indexPath = IndexPath(row: 0, section: 0) - object.tableView.scrollToRow(at: indexPath, at: .top, animated: true) - } - .disposed(by: self.disposeBag) - } - - - // MARK: ReactorKit - bind - - func bind(reactor: MainHomeDistanceViewReactor) { - - // Action - self.rx.viewWillAppear - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let isLoading = reactor.state.map(\.isLoading).distinctUntilChanged().share() - // isLoading == true && isRefreshing == false 일 때, 이벤트 무시 - self.tableView.refreshControl?.rx.controlEvent(.valueChanged) - .withLatestFrom(isLoading) - .filter { $0 == false } - .map { _ in Reactor.Action.refresh } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - // State - isLoading - .do(onNext: { [weak self] isLoading in - if isLoading { self?.isLoadingMore = false } - }) - .subscribe(with: self.tableView) { tableView, isLoading in - if isLoading { - tableView.refreshControl?.beginRefreshingFromTop() - } else { - tableView.refreshControl?.endRefreshing() - } - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.isProcessing) - .distinctUntilChanged() - .do(onNext: { [weak self] isProcessing in - if isProcessing { self?.isLoadingMore = false } - }) - .bind(to: self.activityIndicatorView.rx.isAnimating) - .disposed(by: self.disposeBag) - - reactor.state.map(\.displayedCardsWithUpdate) - .filterNil() - .distinctUntilChanged(reactor.canUpdateCells) - .subscribe(with: self) { object, displayedCardsWithUpdate in - let displayedCards = displayedCardsWithUpdate.cards - let hasMoreUpdate = displayedCardsWithUpdate.hasMoreUpdate - - object.tableView.isHidden = false - - // hasMoreUpdate == true일 때, 추가된 데이터만 로드 - if hasMoreUpdate { - - let lastSectionIndex = object.tableView.numberOfSections - 1 - let lastRowIndex = object.tableView.numberOfRows(inSection: lastSectionIndex) - 1 - let loadedDisplayedCards = displayedCards[0...lastRowIndex] - let indexPathForInsert = displayedCards.enumerated() - .filter { loadedDisplayedCards.contains($0.element) == false } - .map { IndexPath(row: $0.offset, section: 0) } - - object.tableView.performBatchUpdates { - object.tableView.insertRows(at: indexPathForInsert, with: .fade) - } - } else { - - object.tableView.reloadData() - } - } - .disposed(by: self.disposeBag) - } -} - -extension MainHomeDistanceViewController { - - private func cellForPlaceholder(_ tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { - - let placeholder = tableView.dequeueReusableCell( - withIdentifier: "placeholder", - for: indexPath - ) as! PlaceholderViewCell - - return placeholder - } - - private func cellForMainHome( - _ tableView: UITableView, - for indexPath: IndexPath, - with reactor: MainHomeDistanceViewReactor - ) -> UITableViewCell { - - let displayedCards = reactor.currentState.displayedCards - let model = SOMCardModel(data: displayedCards[indexPath.row]) - let cell: MainHomeViewCell = tableView.dequeueReusableCell( - withIdentifier: "cell", - for: indexPath - ) as! MainHomeViewCell - cell.setModel(model) - // 카드 하단 contents 스택 순서 변경 (최신순) - cell.changeOrderInCardContentStack(2) - - return cell - } -} - - -// MARK: MainHomeViewController DataSource and Delegate - -extension MainHomeDistanceViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.reactor?.currentState.displayedCardsCount ?? 1 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - guard let reactor = self.reactor else { return .init(frame: .zero) } - - if reactor.currentState.isDisplayedCardsEmpty { - return self.cellForPlaceholder(tableView, for: indexPath) - } else { - return self.cellForMainHome(tableView, for: indexPath, with: reactor) - } - } -} - -extension MainHomeDistanceViewController: UITableViewDataSourcePrefetching { - - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - guard let reactor = self.reactor else { return } - - indexPaths.forEach { indexPath in - // 데이터 로드 전, 이미지 캐싱 - let strUrl = reactor.currentState.displayedCards[indexPath.row].backgroundImgURL.url - KingfisherManager.shared.download(strUrl: strUrl) { _ in } - } - } -} - -extension MainHomeDistanceViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let reactor = self.reactor else { return } - - let selectedId = reactor.currentState.displayedCards[indexPath.row].id - self.willPushCardId.accept(selectedId) - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return (self.reactor?.currentState.isDisplayedCardsEmpty ?? true) ? tableView.bounds.height : self.cellHeight - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard self.reactor?.currentState.isDisplayedCardsEmpty == false else { return } - - let lastSectionIndex = tableView.numberOfSections - 1 - let lastRowIndex = tableView.numberOfRows(inSection: lastSectionIndex) - 1 - - if self.isLoadingMore, - indexPath.section == lastSectionIndex, - indexPath.row == lastRowIndex, - let reactor = self.reactor { - - let lastId = reactor.currentState.displayedCards[indexPath.row].id - reactor.action.onNext(.moreFind(lastId)) - } - } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - - // currentOffset <= 0 && isLoading == false 일 때, 테이블 뷰 새로고침 가능 - self.isRefreshEnabled = (self.currentOffset <= 0 && self.reactor?.currentState.isLoading == false) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - - let offset = scrollView.contentOffset.y - - // 당겨서 새로고침 상황일 때 - guard offset > 0 else { - - self.hidesHeaderContainer.accept(false) - self.currentOffset = offset - self.moveTopButton.isHidden = true - - return - } - - guard offset <= (scrollView.contentSize.height - scrollView.frame.height) else { return } - - // offset이 currentOffset보다 크면 아래로 스크롤, 반대일 경우 위로 스크롤 - // 위로 스크롤 중일 때 헤더뷰 표시, 아래로 스크롤 중일 때 헤더뷰 숨김 - self.hidesHeaderContainer.accept(offset > self.currentOffset) - - // 아래로 스크롤 중일 때, 데이터 추가로드 가능 - self.isLoadingMore = offset > self.currentOffset - - self.currentOffset = offset - - // 최상단일 때만 moveToButton 숨김 - self.moveTopButton.isHidden = self.currentOffset <= 0 - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - - let offset = scrollView.contentOffset.y - - // isRefreshEnabled == true 이고, 스크롤이 끝났을 경우에만 테이블 뷰 새로고침 - if self.isRefreshEnabled, - let refreshControl = self.tableView.refreshControl, - offset <= -(refreshControl.frame.origin.y + 40) { - - refreshControl.beginRefreshingFromTop() - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Distance/MainHomeDistanceViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Distance/MainHomeDistanceViewReactor.swift deleted file mode 100644 index 8fd5777e..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Distance/MainHomeDistanceViewReactor.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// MainHomeDistanceViewReactor.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import ReactorKit - - -class MainHomeDistanceViewReactor: Reactor { - - // hasMoreUpdate == true 일 때, more - typealias CardsWithUpdate = (cards: [Card], hasMoreUpdate: Bool) - - enum Action: Equatable { - case landing - case refresh - case moreFind(String) - case distanceFilter(String) - } - - enum Mutation { - case cards(CardsWithUpdate) - case more(CardsWithUpdate) - case updateDistanceFilter(String) - case updateIsLoading(Bool) - case updateIsProcessing(Bool) - } - - struct State { - fileprivate(set) var displayedCardsWithUpdate: CardsWithUpdate? - fileprivate(set) var distanceFilter: String - fileprivate(set) var isLoading: Bool - fileprivate(set) var isProcessing: Bool - - var displayedCards: [Card] { - return self.displayedCardsWithUpdate?.cards ?? [] - } - var isDisplayedCardsEmpty: Bool { - return self.displayedCards.isEmpty - } - var displayedCardsCount: Int { - return self.isDisplayedCardsEmpty ? 1 : self.displayedCards.count - } - } - - var initialState: State = .init( - displayedCardsWithUpdate: nil, - distanceFilter: "UNDER_1", - isLoading: false, - isProcessing: false - ) - - let provider: ManagerProviderType - - // TODO: 페이징 - // private let countPerLoading: Int = 10 - - init(provider: ManagerProviderType) { - self.provider = provider - } - - - func mutate(action: Action) -> Observable { - switch action { - case .landing: - - return .concat([ - .just(.updateIsProcessing(true)), - self.refresh(self.currentState.distanceFilter) - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsProcessing(false)) - ]) - case .refresh: - - return .concat([ - .just(.updateIsLoading(true)), - self.refresh(self.currentState.distanceFilter) - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsLoading(false)) - ]) - case let .moreFind(lastId): - - return .concat([ - .just(.updateIsProcessing(true)), - self.moreFind(lastId), - .just(.updateIsProcessing(false)) - ]) - case let .distanceFilter(distanceFilter): - - return .concat([ - .just(.updateIsProcessing(true)), - .just(.updateDistanceFilter(distanceFilter)), - self.refresh(distanceFilter) - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsProcessing(false)) - ]) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var state: State = state - switch mutation { - case let .cards(displayedCardsWithUpdate): - state.displayedCardsWithUpdate = displayedCardsWithUpdate - case let .more(displayedCardsWithUpdate): - state.displayedCardsWithUpdate?.cards += displayedCardsWithUpdate.cards - state.displayedCardsWithUpdate?.hasMoreUpdate = displayedCardsWithUpdate.hasMoreUpdate - case let .updateDistanceFilter(distanceFilter): - state.distanceFilter = distanceFilter - case let .updateIsLoading(isLoading): - state.isLoading = isLoading - case let .updateIsProcessing(isProcessing): - state.isProcessing = isProcessing - } - return state - } -} - -extension MainHomeDistanceViewReactor { - - func refresh(_ distanceFilter: String) -> Observable { - - let latitude = self.provider.locationManager.coordinate.latitude - let longitude = self.provider.locationManager.coordinate.longitude - - let request: CardRequest = .distancCard( - lastId: nil, - latitude: latitude, - longitude: longitude, - distanceFilter: distanceFilter - ) - return self.provider.networkManager.request(DistanceCardResponse.self, request: request) - .map(\.embedded.cards) - .map { Mutation.cards((cards: $0, hasMoreUpdate: false)) } - .catch(self.catchClosure) - } - - func moreFind(_ lastId: String) -> Observable { - - let latitude = self.provider.locationManager.coordinate.latitude - let longitude = self.provider.locationManager.coordinate.longitude - - let distanceFilter = self.currentState.distanceFilter - - let request: CardRequest = .distancCard( - lastId: lastId, - latitude: latitude, - longitude: longitude, - distanceFilter: distanceFilter - ) - return self.provider.networkManager.request(DistanceCardResponse.self, request: request) - .map(\.embedded.cards) - .map { Mutation.more((cards: $0, hasMoreUpdate: true)) } - .catch(self.catchClosure) - } -} - -extension MainHomeDistanceViewReactor { - - var catchClosure: ((Error) throws -> Observable ) { - return { _ in - .concat([ - .just(.cards((cards: [], hasMoreUpdate: false))), - .just(.updateIsProcessing(false)), - .just(.updateIsLoading(false)) - ]) - } - } - - // TODO: 페이징 - // func separate(displayed displayedCards: [Card], current cards: [Card]) -> [Card] { - // let count = displayedCards.count - // let displayedCards = Array(cards[count.. Bool { - return prevCardsWithUpdate.cards == currCardsWithUpdate.cards && - prevCardsWithUpdate.hasMoreUpdate == currCardsWithUpdate.hasMoreUpdate - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift new file mode 100644 index 00000000..c07723b8 --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewController.swift @@ -0,0 +1,662 @@ +// +// HomeViewController.swift +// SOOUM +// +// Created by 오현식 on 9/22/25. +// + +import UIKit + +import SnapKit +import Then + +import ReactorKit +import RxCocoa +import RxSwift + +class HomeViewController: BaseNavigationViewController, View { + + enum Text { + static let tabLatestTitle: String = "최신카드" + static let tabPopularityTitle: String = "인기카드" + static let tabDistanceTitle: String = "주변카드" + + static let distanceFilternder1km: String = "1km" + static let distanceFilternder5km: String = "5km" + static let distanceFilternder10km: String = "10km" + static let distanceFilternder20km: String = "20km" + static let distanceFilternder50km: String = "50km" + + static let dialogTitle: String = "위치 정보 사용 설정" + static let dialogMessage: String = "내 위치 확인을 위해 ‘설정 > 앱 > 숨 > 위치’에서 위치 정보 사용을 허용해 주세요." + + static let cancelActionTitle: String = "취소" + static let settingActionTitle: String = "설정" + } + + enum Section: Int, CaseIterable { + case latest + case popular + case distance + case empty + } + + enum Item: Hashable { + case latest(BaseCardInfo) + case popular(BaseCardInfo) + case distance(BaseCardInfo) + case empty + } + + + // MARK: Views + + private let logo = UIImageView().then { + $0.image = .init(.logo(.v2(.logo_black))) + $0.contentMode = .scaleAspectFit + } + + private let rightAlamButton = SOMButton().then { + $0.image = .init(.icon(.v2(.outlined(.bell)))) + $0.foregroundColor = .som.v2.black + } + + private let dotWithoutReadView = UIView().then { + $0.backgroundColor = .som.v2.rMain + $0.layer.cornerRadius = 5 * 0.5 + $0.isHidden = true + } + + private let headerContainer = UIStackView().then { + $0.backgroundColor = .som.v2.white + $0.axis = .vertical + } + + private lazy var stickyTabBar = SOMStickyTabBar().then { + $0.items = [Text.tabLatestTitle, Text.tabPopularityTitle, Text.tabDistanceTitle] + $0.spacing = 24 + $0.delegate = self + } + + private lazy var distanceFilterView = SOMSwipableTabBar().then { + $0.items = [ + Text.distanceFilternder1km, + Text.distanceFilternder5km, + Text.distanceFilternder10km, + Text.distanceFilternder20km, + Text.distanceFilternder50km + ] + $0.isHidden = true + $0.delegate = self + } + + private lazy var topNoticeView = SOMPageViews().then { + $0.delegate = self + } + + private lazy var tableView = UITableView().then { + $0.backgroundColor = .som.v2.gray100 + $0.indicatorStyle = .black + $0.separatorStyle = .none + + $0.contentInset.top = self.headerViewHeight + 16 + $0.contentInset.bottom = 54 + 16 + + $0.verticalScrollIndicatorInsets.bottom = 54 + + $0.isHidden = true + + $0.refreshControl = SOMRefreshControl() + + $0.register(HomeViewCell.self, forCellReuseIdentifier: HomeViewCell.cellIdentifier) + $0.register(HomePlaceholderViewCell.self, forCellReuseIdentifier: HomePlaceholderViewCell.cellIdentifier) + + $0.delegate = self + } + + + // MARK: Variables + + typealias DataSource = UITableViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + + private lazy var dataSource = DataSource(tableView: self.tableView) { [weak self] tableView, indexPath, item -> UITableViewCell? in + + guard let self = self else { return nil } + + switch item { + case let .latest(cardInfo): + + let cell: HomeViewCell = self.cellForCard(tableView, with: indexPath) + cell.bind(cardInfo) + + return cell + case let .popular(cardInfo): + + let cell: HomeViewCell = self.cellForCard(tableView, with: indexPath) + cell.bind(cardInfo) + + return cell + case let .distance(cardInfo): + + let cell: HomeViewCell = self.cellForCard(tableView, with: indexPath) + cell.bind(cardInfo) + + return cell + case .empty: + + return self.cellForPlaceholder(tableView, with: indexPath) + } + } + + private var isRefreshEnabled: Bool = true + private var shouldRefreshing: Bool = false + private var isLoadingMore: Bool = false + + private var hidesHeaderView: Bool = false + private var headerViewHeight: CGFloat = SOMStickyTabBar.Constants.height + private var initialOffset: CGFloat = 0 + private var currentOffset: CGFloat = 0 + + private var cellHeight: CGFloat { + let width: CGFloat = (UIScreen.main.bounds.width - 16 * 2) * 0.5 + /// (가로 : 세로 = 2 : 1) + bottom contents container height + bottom inset + return width + 34 + 10 + } + + + // MARK: Constraints + + private var headerViewContainerTopConstraint: Constraint? + + + // MARK: Variables + Rx + + let willPushCardId = PublishRelay() + + + // MARK: Override func + + override func viewDidLoad() { + super.viewDidLoad() + + // 제스처 뒤로가기를 위한 델리게이트 설정 + self.navigationController?.interactivePopGestureRecognizer?.delegate = self + + NotificationCenter.default.addObserver( + self, + selector: #selector(self.scollingToTopWithAnimation(_:)), + name: .scollingToTopWithAnimation, + object: nil + ) + } + + override func bind() { + super.bind() + #if DEVELOP + self.setupDebugging() + #endif + } + + override func setupNaviBar() { + super.setupNaviBar() + + self.navigationBar.titleView = self.logo + self.navigationBar.titlePosition = .left + + self.navigationBar.hidesBackButton = true + + self.rightAlamButton.addSubview(self.dotWithoutReadView) + self.dotWithoutReadView.snp.makeConstraints { + $0.top.equalToSuperview().offset(12) + $0.trailing.equalToSuperview().offset(-12) + $0.size.equalTo(5) + } + self.rightAlamButton.snp.makeConstraints { + $0.size.equalTo(48) + } + + self.navigationBar.setRightButtons([self.rightAlamButton]) + } + + override func setupConstraints() { + super.setupConstraints() + + self.view.addSubview(self.tableView) + self.tableView.snp.makeConstraints { + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) + $0.bottom.horizontalEdges.equalToSuperview() + } + + self.view.addSubview(self.headerContainer) + self.headerContainer.snp.makeConstraints { + self.headerViewContainerTopConstraint = $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).priority(.high).constraint + $0.horizontalEdges.equalToSuperview() + } + self.headerContainer.addArrangedSubview(self.stickyTabBar) + self.headerContainer.addArrangedSubview(self.distanceFilterView) + } + + + // MARK: ReactorKit - bind + + func bind(reactor: HomeViewReactor) { + + // navigation + self.rightAlamButton.rx.throttleTap() + .subscribe(with: self) { object, _ in + let viewController = NotificationViewController() + viewController.reactor = reactor.reactorForNotification() + object.navigationPush(viewController, animated: true, bottomBarHidden: true) + } + .disposed(by: self.disposeBag) + + // tabBar 표시 + self.rx.viewDidAppear + .subscribe(with: self) { object, _ in + object.hidesBottomBarWhenPushed = false + } + .disposed(by: self.disposeBag) + + // Action + self.rx.viewDidLoad + .map { _ in Reactor.Action.landing } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + let isRefreshing = reactor.state.map(\.isRefreshing).distinctUntilChanged().share() + self.tableView.refreshControl?.rx.controlEvent(.valueChanged) + .withLatestFrom(isRefreshing) + .filter { $0 == false } + .delay(.milliseconds(1000), scheduler: MainScheduler.instance) + .map { _ in Reactor.Action.refresh } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + // State + isRefreshing + .observe(on: MainScheduler.asyncInstance) + .filter { $0 == false } + .subscribe(with: self.tableView) { tableView, _ in + tableView.refreshControl?.endRefreshing() + } + .disposed(by: self.disposeBag) + + reactor.state.map(\.hasUnreadNotifications) + .distinctUntilChanged() + .map { $0 == false } + .bind(to: self.dotWithoutReadView.rx.isHidden) + .disposed(by: self.disposeBag) + + reactor.state.map(\.noticeInfos) + .filterNil() + .distinctUntilChanged() + .subscribe(with: self) { object, noticeInfos in + let models: [SOMPageModel] = noticeInfos + .enumerated() + .map { SOMPageModel(data: $1, index: ($0, noticeInfos.count)) } + object.topNoticeView.frame = CGRect(origin: .zero, size: .init(width: UIScreen.main.bounds.width, height: 81)) + object.topNoticeView.setModels(models) + object.tableView.tableHeaderView = noticeInfos.isEmpty ? nil : object.topNoticeView + } + .disposed(by: self.disposeBag) + + reactor.state.map { + HomeViewReactor.DisplayStates( + displayType: $0.displayType, + latests: $0.latestCards, + populars: $0.popularCards, + distances: $0.distanceCards + ) + } + .observe(on: MainScheduler.asyncInstance) + .subscribe(with: self) { object, displayStats in + + var snapshot = Snapshot() + snapshot.appendSections(Section.allCases) + + switch displayStats.displayType { + case .latest: + + guard let latests = displayStats.latests else { return } + + guard latests.isEmpty == false else { + snapshot.appendItems([.empty], toSection: .empty) + break + } + + let new = latests.map { Item.latest($0) } + snapshot.appendItems(new, toSection: .latest) + case .popular: + + guard let populars = displayStats.populars else { return } + + guard populars.isEmpty == false else { + snapshot.appendItems([.empty], toSection: .empty) + break + } + + let new = populars.map { Item.popular($0) } + snapshot.appendItems(new, toSection: .popular) + case .distance: + + guard let distances = displayStats.distances else { return } + + guard distances.isEmpty == false else { + snapshot.appendItems([.empty], toSection: .empty) + break + } + + let new = distances.map { Item.distance($0) } + snapshot.appendItems(new, toSection: .distance) + } + + object.dataSource.apply(snapshot, animatingDifferences: false) + + object.tableView.isHidden = false + } + .disposed(by: self.disposeBag) + } + + + // MARK: Objc func + + @objc + private func scollingToTopWithAnimation(_ notification: Notification) { + + guard let displayType = self.reactor?.currentState.displayType else { return } + + var section: Int { + switch displayType { + case .latest: return Section.latest.rawValue + case .popular: return Section.popular.rawValue + case .distance: return Section.distance.rawValue + } + } + + let toTop = CGPoint(x: 0, y: -(self.tableView.contentInset.top)) + self.tableView.setContentOffset(toTop, animated: true) + } +} + + +// MARK: Cells + +private extension HomeViewController { + + func cellForPlaceholder( + _ tableView: UITableView, + with indexPath: IndexPath + ) -> HomePlaceholderViewCell { + + return tableView.dequeueReusableCell( + withIdentifier: HomePlaceholderViewCell.cellIdentifier, + for: indexPath + ) as! HomePlaceholderViewCell + } + + func cellForCard( + _ tableView: UITableView, + with indexPath: IndexPath + ) -> HomeViewCell { + + return tableView.dequeueReusableCell( + withIdentifier: HomeViewCell.cellIdentifier, + for: indexPath + ) as! HomeViewCell + } +} + + +// MARK: show Dialog + +private extension HomeViewController { + + func showLocationPermissionDialog() { + + let cancelAction = SOMDialogAction( + title: Text.cancelActionTitle, + style: .gray, + action: { + UIApplication.topViewController?.dismiss(animated: true) + } + ) + let settingAction = SOMDialogAction( + title: Text.settingActionTitle, + style: .primary, + action: { + let application = UIApplication.shared + let openSettingsURLString: String = UIApplication.openSettingsURLString + if let settingsURL = URL(string: openSettingsURLString), + application.canOpenURL(settingsURL) { + application.open(settingsURL) + } + + UIApplication.topViewController?.dismiss(animated: true) + } + ) + + SOMDialogViewController.show( + title: Text.dialogTitle, + message: Text.dialogMessage, + actions: [cancelAction, settingAction] + ) + } +} + + +// MARK: SOMStickyTabBarDelegate + +extension HomeViewController: SOMStickyTabBarDelegate { + + func tabBar(_ tabBar: SOMStickyTabBar, shouldSelectTabAt index: Int) -> Bool { + + if index == 2, self.reactor?.locationManager.checkLocationAuthStatus() == .denied { + + self.showLocationPermissionDialog() + return false + } + + return true + } + + func tabBar(_ tabBar: SOMStickyTabBar, didSelectTabAt index: Int) { + + let hidesDistanceFilter = index != 2 + self.distanceFilterView.isHidden = hidesDistanceFilter + self.headerViewHeight = hidesDistanceFilter ? + SOMStickyTabBar.Constants.height : + SOMStickyTabBar.Constants.height + SOMSwipableTabBar.Constants.height + self.tableView.contentInset.top = self.headerViewHeight + 16 + + let toTop = CGPoint(x: 0, y: -(self.headerViewHeight + 16)) + self.tableView.setContentOffset(toTop, animated: false) + + var displayType: HomeViewReactor.DisplayType { + switch index { + case 1: return .popular + case 2: return .distance + default: return .latest + } + } + self.reactor?.action.onNext(.updateDisplayType(displayType)) + } +} + + +// MARK: SOMSwipeTabBarDelegate + +extension HomeViewController: SOMSwipableTabBarDelegate { + + func tabBar(_ tabBar: SOMSwipableTabBar, didSelectTabAt index: Int) { + + let distanceFilter = tabBar.items[index] + self.reactor?.action.onNext(.updateDistanceFilter(distanceFilter)) + } +} + + +extension HomeViewController: SOMPageViewsDelegate { + + func pages(_ tags: SOMPageViews, didTouch model: SOMPageModel) { + + guard let reactorForNotification = self.reactor?.reactorForNotification(with: .notice) else { return } + + let viewController = NotificationViewController() + viewController.reactor = reactorForNotification + self.navigationPush(viewController, animated: true, bottomBarHidden: true) + } +} + + +// MARK: UITableViewDelegate + +extension HomeViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + // TODO: 상세보기 화면 전환 필요 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + + guard let item = self.dataSource.itemIdentifier(for: indexPath) else { + return tableView.bounds.height + } + + switch item { + case .empty: + return tableView.bounds.height + default: + return self.cellHeight + } + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + + guard let reactor = self.reactor, reactor.currentState.isRefreshing == false else { return } + + switch reactor.currentState.displayType { + case .latest: + + let lastRowIndexPath = tableView.numberOfRows(inSection: Section.latest.rawValue) - 1 + if self.isLoadingMore, + reactor.currentState.latestCards?.isEmpty == false, + indexPath.section == Section.latest.rawValue, + indexPath.row == lastRowIndexPath { + + let lastId = reactor.currentState.latestCards?.last?.id ?? "" + reactor.action.onNext(.moreFind(lastId)) + } + case .distance: + + let lastRowIndexPath = tableView.numberOfRows(inSection: Section.distance.rawValue) - 1 + if self.isLoadingMore, + reactor.currentState.distanceCards?.isEmpty == false, + indexPath.section == Section.distance.rawValue, + indexPath.row == lastRowIndexPath { + + let lastId = reactor.currentState.distanceCards?.last?.id ?? "" + reactor.action.onNext(.moreFind(lastId)) + } + default: + return + } + } + + + // MARK: UIScrollViewDelegate + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + + let offset = scrollView.contentOffset.y + + // currentOffset <= 0 && isLoading == false 일 때, 테이블 뷰 새로고침 가능 + self.isRefreshEnabled = (offset <= 0) && (self.reactor?.currentState.isRefreshing == false) + self.shouldRefreshing = false + self.initialOffset = offset + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + + let offset = scrollView.contentOffset.y + let delta = offset - self.currentOffset + + let isScrollingDown = delta > 0 + + // 당겨서 새로고침 + if self.isRefreshEnabled, offset < self.initialOffset { + guard let refreshControl = self.tableView.refreshControl else { + self.currentOffset = offset + return + } + + let pulledOffset = self.initialOffset - offset + let refreshingOffset = refreshControl.frame.origin.y + refreshControl.frame.height + 16 + self.shouldRefreshing = abs(pulledOffset) >= refreshingOffset + } + + // 당겨서 새로고침 시 무시 + guard offset > 0, + // 스크롤이 맨 아래에 도달했을 때, 헤더뷰 숨김 로직을 무시 + offset <= (scrollView.contentSize.height - scrollView.frame.height) + else { + self.currentOffset = offset + return + } + + if isScrollingDown { + // 현재 constraint를 직접 비교 + let currentTopConstraint = self.headerViewContainerTopConstraint?.layoutConstraints.first?.constant ?? 0 + let targetOffset = max(-self.headerViewHeight, currentTopConstraint - delta) + self.headerViewContainerTopConstraint?.update(offset: targetOffset).update(priority: .high) + } + + if isScrollingDown == false { + + self.headerViewContainerTopConstraint?.update(offset: 0).update(priority: .high) + } + + UIView.animate(withDuration: 0.2) { + self.view.layoutIfNeeded() + } + + // 아래로 스크롤 중일 때, 데이터 추가로드 가능 + self.isLoadingMore = isScrollingDown + + self.currentOffset = offset + } + + func scrollViewWillEndDragging( + _ scrollView: UIScrollView, + withVelocity velocity: CGPoint, + targetContentOffset: UnsafeMutablePointer + ) { + + if self.shouldRefreshing { + self.tableView.refreshControl?.beginRefreshingFromTop() + } + } +} + + +// MARK: Download log history for debugging + +private extension HomeViewController { + + func setupDebugging() { + + self.logo.rx.longPressGesture() + .when(.began) + .flatMapLatest { _ in Log.extract() } + .subscribe( + with: self, + onNext: { object, viewController in + object.navigationController?.present(viewController, animated: true) + }, + onError: { _, error in + Log.error(error.localizedDescription) + } + ) + .disposed(by: self.disposeBag) + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift new file mode 100644 index 00000000..b5e939fb --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Home/HomeViewReactor.swift @@ -0,0 +1,278 @@ +// +// HomeViewReactor.swift +// SOOUM +// +// Created by 오현식 on 9/28/25. +// + +import ReactorKit + +import Alamofire + +class HomeViewReactor: Reactor { + + struct DisplayStates { + let displayType: DisplayType + let latests: [BaseCardInfo]? + let populars: [BaseCardInfo]? + let distances: [BaseCardInfo]? + } + + enum DisplayType: Equatable { + case latest + case popular + case distance + } + + enum Action: Equatable { + case landing + case refresh + case moreFind(String) + case updateDisplayType(DisplayType) + case updateDistanceFilter(String) + } + + enum Mutation { + case cards([BaseCardInfo]) + case more([BaseCardInfo]) + case updateHasUnreadNotifications(Bool) + case notices([NoticeInfo]) + case updateDisplayType(DisplayType) + case updateDistanceFilter(String) + case updateIsRefreshing(Bool) + } + + struct State { + fileprivate(set) var displayType: DisplayType + fileprivate(set) var noticeInfos: [NoticeInfo]? + fileprivate(set) var latestCards: [BaseCardInfo]? + fileprivate(set) var popularCards: [BaseCardInfo]? + fileprivate(set) var distanceCards: [BaseCardInfo]? + fileprivate(set) var hasUnreadNotifications: Bool + fileprivate(set) var distanceFilter: String + fileprivate(set) var isRefreshing: Bool + } + + var initialState: State + + private let dependencies: AppDIContainerable + private let cardUseCase: CardUseCase + private let notificationUseCase: NotificationUseCase + + let locationManager: LocationManagerDelegate + + init(dependencies: AppDIContainerable, displayType: DisplayType = .latest) { + self.dependencies = dependencies + self.cardUseCase = dependencies.rootContainer.resolve(CardUseCase.self) + self.notificationUseCase = dependencies.rootContainer.resolve(NotificationUseCase.self) + + self.locationManager = dependencies.rootContainer.resolve(ManagerProviderType.self).locationManager + + self.initialState = State( + displayType: displayType, + noticeInfos: nil, + latestCards: nil, + popularCards: nil, + distanceCards: nil, + hasUnreadNotifications: false, + distanceFilter: "lkm", + isRefreshing: false + ) + } + + + func mutate(action: Action) -> Observable { + switch action { + case .landing: + + let displayType = self.currentState.displayType + let distanceFilter = self.currentState.distanceFilter + return .concat([ + self.refresh(displayType, distanceFilter) + .catch(self.catchClosure), + // self.unreadNotifications() + .just(.notices([ + NoticeInfo(id: "1", noticeType: .news, message: "숨이 새로운 서비스로 찾아올 예정이에요", url: "", createdAt: Date()), + NoticeInfo(id: "2", noticeType: .announcement, message: "숨 공식 인스타그램 안내드려요", url: "", createdAt: Date()), + NoticeInfo(id: "3", noticeType: .maintenance, message: "카드 작성 시 발생했던 오류가 해결됐어요", url: "", createdAt: Date()) + ])), + .just(.updateHasUnreadNotifications(true)) + ]) + case .refresh: + + let displayType = self.currentState.displayType + let distanceFilter = self.currentState.distanceFilter + return .concat([ + .just(.updateIsRefreshing(true)), + self.refresh(displayType, distanceFilter) + .catch(self.catchClosure), + self.unreadNotifications(), + .just(.updateIsRefreshing(false)) + ]) + case let .moreFind(lastId): + + return self.moreFind(lastId) + case let .updateDisplayType(displayType): + + let distanceFilter = self.currentState.distanceFilter + var emitObservable: Observable { + switch displayType { + case .latest: + if let latestCards = self.currentState.latestCards { + return .just(.cards(latestCards)) + } else { + return self.refresh(.latest, distanceFilter) + .catch(self.catchClosure) + } + case .popular: + if let popularCards = self.currentState.popularCards { + return .just(.cards(popularCards)) + } else { + return self.refresh(.popular, distanceFilter) + .catch(self.catchClosure) + } + case .distance: + if let distanceCards = self.currentState.distanceCards { + return .just(.cards(distanceCards)) + } else { + return self.refresh(.distance, distanceFilter) + .catch(self.catchClosure) + } + } + } + + return .concat([ + .just(.updateDisplayType(displayType)), + emitObservable + ]) + case let .updateDistanceFilter(distanceFilter): + + let displayType = self.currentState.displayType + return .concat([ + .just(.updateDistanceFilter(distanceFilter)), + self.refresh(displayType, distanceFilter) + ]) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .cards(cards): + switch newState.displayType { + case .latest: newState.latestCards = cards + case .popular: newState.popularCards = cards + case .distance: newState.distanceCards = cards + } + case let .more(cards): + switch newState.displayType { + case .latest: newState.latestCards? += cards + case .distance: newState.distanceCards? += cards + default: break + } + case let .notices(noticeInfos): + newState.noticeInfos = noticeInfos + case let .updateHasUnreadNotifications(hasUnreadNotifications): + newState.hasUnreadNotifications = hasUnreadNotifications + case let .updateDisplayType(displayType): + newState.displayType = displayType + case let .updateDistanceFilter(distanceFilter): + newState.distanceFilter = distanceFilter + case let .updateIsRefreshing(isRefreshing): + newState.isRefreshing = isRefreshing + } + return newState + } +} + +private extension HomeViewReactor { + + func refresh(_ displayType: DisplayType, _ distanceFilter: String) -> Observable { + + let latitude = self.locationManager.coordinate.latitude + let longitude = self.locationManager.coordinate.longitude + + switch displayType { + case .latest: + return self.cardUseCase.latestCard(lastId: nil, latitude: latitude, longitude: longitude) + .map(Mutation.cards) + case .popular: + return self.cardUseCase.popularCard(latitude: latitude, longitude: longitude) + .map(Mutation.cards) + case .distance: + let distanceFilter = distanceFilter.replacingOccurrences(of: "km", with: "") + return self.cardUseCase.distanceCard( + lastId: nil, + latitude: latitude, + longitude: longitude, + distanceFilter: distanceFilter + ) + .map(Mutation.cards) + } + } + + func moreFind(_ lastId: String) -> Observable { + + let latitude = self.locationManager.coordinate.latitude + let longitude = self.locationManager.coordinate.longitude + + switch self.currentState.displayType { + case .latest: + return self.cardUseCase.latestCard(lastId: lastId, latitude: latitude, longitude: longitude) + .map(Mutation.more) + case .distance: + return self.cardUseCase.distanceCard( + lastId: lastId, + latitude: latitude, + longitude: longitude, + distanceFilter: self.currentState.distanceFilter + ) + .map(Mutation.more) + default: + return .empty() + } + } + + func unreadNotifications() -> Observable { + + return self.notificationUseCase.notices(lastId: nil) + .flatMapLatest { noticeInfos -> Observable in + + return .concat([ + self.notificationUseCase.unreadNotifications(lastId: nil) + .map { .updateHasUnreadNotifications($0.isEmpty == false && noticeInfos.isEmpty == false) }, + .just(.notices(noticeInfos)) + ]) + } + } +} + +extension HomeViewReactor { + + var catchClosure: ((Error) throws -> Observable ) { + return { _ in + .concat([ + .just(.cards([])), + .just(.updateIsRefreshing(false)) + ]) + } + } + + func canUpdateCells( + prev prevDisplayState: DisplayStates, + curr currDisplayState: DisplayStates + ) -> Bool { + return prevDisplayState.displayType == currDisplayState.displayType && + prevDisplayState.latests == currDisplayState.latests && + prevDisplayState.populars == currDisplayState.populars && + prevDisplayState.distances == currDisplayState.distances + } +} + + +extension HomeViewReactor { + + func reactorForNotification(with displayType: NotificationViewReactor.DisplayType = .activity(.unread)) -> NotificationViewReactor { + NotificationViewReactor(dependencies: self.dependencies, displayType: displayType) + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Latest/MainHomeLatestViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Latest/MainHomeLatestViewController.swift deleted file mode 100644 index 2cc40f46..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Latest/MainHomeLatestViewController.swift +++ /dev/null @@ -1,323 +0,0 @@ -// -// MainHomeLatestViewController.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import UIKit - -import Kingfisher -import SnapKit -import Then - -import ReactorKit -import RxCocoa -import RxSwift - - -class MainHomeLatestViewController: BaseViewController, View { - - - // MARK: Views - - private lazy var tableView = UITableView(frame: .zero, style: .plain).then { - $0.backgroundColor = .clear - $0.indicatorStyle = .black - $0.separatorStyle = .none - - $0.contentInset.top = SOMSwipeTabBar.Height.mainHome - - $0.isHidden = true - - $0.register(MainHomeViewCell.self, forCellReuseIdentifier: "cell") - $0.register(PlaceholderViewCell.self, forCellReuseIdentifier: "placeholder") - - $0.refreshControl = SOMRefreshControl() - - $0.dataSource = self - $0.prefetchDataSource = self - - $0.delegate = self - } - - private let moveTopButton = MoveTopButtonView().then { - $0.isHidden = true - } - - - // MARK: Variables - - // tableView 정보 - private var currentOffset: CGFloat = 0 - private var isRefreshEnabled: Bool = true - private var isLoadingMore: Bool = false - - private let cellHeight: CGFloat = { - let width: CGFloat = (UIScreen.main.bounds.width - 20 * 2) * 0.9 - return width + 10 /// 가로 + top inset - }() - - - // MARK: Variables + Rx - - let hidesHeaderContainer = PublishRelay() - let willPushCardId = PublishRelay() - - - // MARK: Override func - - override func setupConstraints() { - super.setupConstraints() - - self.view.addSubview(self.tableView) - self.tableView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - self.view.addSubview(self.moveTopButton) - self.view.bringSubviewToFront(self.moveTopButton) - self.moveTopButton.snp.makeConstraints { - let bottomOffset: CGFloat = 24 + 60 + 4 + 20 - $0.bottom.equalTo(self.tableView.snp.bottom).offset(-bottomOffset) - $0.centerX.equalToSuperview() - $0.height.equalTo(MoveTopButtonView.height) - } - } - - override func bind() { - super.bind() - - // tableView 상단 이동 - self.moveTopButton.backgroundButton.rx.throttleTap(.seconds(3)) - .subscribe(with: self) { object, _ in - let indexPath = IndexPath(row: 0, section: 0) - object.tableView.scrollToRow(at: indexPath, at: .top, animated: true) - } - .disposed(by: self.disposeBag) - } - - - // MARK: ReactorKit - bind - - func bind(reactor: MainHomeLatestViewReactor) { - - // Action - self.rx.viewWillAppear - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let isLoading = reactor.state.map(\.isLoading).distinctUntilChanged().share() - // isLoading == true && isRefreshing == false 일 때, 이벤트 무시 - self.tableView.refreshControl?.rx.controlEvent(.valueChanged) - .withLatestFrom(isLoading) - .filter { $0 == false } - .map { _ in Reactor.Action.refresh } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - // State - isLoading - .do(onNext: { [weak self] isLoading in - if isLoading { self?.isLoadingMore = false } - }) - .subscribe(with: self.tableView) { tableView, isLoading in - if isLoading { - tableView.refreshControl?.beginRefreshingFromTop() - } else { - tableView.refreshControl?.endRefreshing() - } - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.isProcessing) - .distinctUntilChanged() - .do(onNext: { [weak self] isProcessing in - if isProcessing { self?.isLoadingMore = false } - }) - .bind(to: self.activityIndicatorView.rx.isAnimating) - .disposed(by: self.disposeBag) - - reactor.state.map(\.displayedCardsWithUpdate) - .filterNil() - .distinctUntilChanged(reactor.canUpdateCells) - .subscribe(with: self) { object, displayedCardsWithUpdate in - let displayedCards = displayedCardsWithUpdate.cards - let hasMoreUpdate = displayedCardsWithUpdate.hasMoreUpdate - - object.tableView.isHidden = false - - // hasMoreUpdate == true일 때, 추가된 데이터만 로드 - if hasMoreUpdate { - - let lastSectionIndex = object.tableView.numberOfSections - 1 - let lastRowIndex = object.tableView.numberOfRows(inSection: lastSectionIndex) - 1 - let loadedDisplayedCards = displayedCards[0...lastRowIndex] - let indexPathForInsert = displayedCards.enumerated() - .filter { loadedDisplayedCards.contains($0.element) == false } - .map { IndexPath(row: $0.offset, section: 0) } - - object.tableView.performBatchUpdates { - object.tableView.insertRows(at: indexPathForInsert, with: .fade) - } - } else { - - object.tableView.reloadData() - } - } - .disposed(by: self.disposeBag) - } -} - - -// MARK: Set UITableView cell - -extension MainHomeLatestViewController { - - private func cellForPlaceholder(_ tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { - - let placeholder = tableView.dequeueReusableCell( - withIdentifier: "placeholder", - for: indexPath - ) as! PlaceholderViewCell - - return placeholder - } - - private func cellForMainHome( - _ tableView: UITableView, - for indexPath: IndexPath, - with reactor: MainHomeLatestViewReactor - ) -> UITableViewCell { - - let displayedCards = reactor.currentState.displayedCards - let model = SOMCardModel(data: displayedCards[indexPath.row]) - let cell: MainHomeViewCell = tableView.dequeueReusableCell( - withIdentifier: "cell", - for: indexPath - ) as! MainHomeViewCell - cell.setModel(model) - // 카드 하단 contents 스택 순서 변경 (최신순) - cell.changeOrderInCardContentStack(0) - - return cell - } -} - - -// MARK: MainHomeViewController DataSource and Delegate - -extension MainHomeLatestViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.reactor?.currentState.displayedCardsCount ?? 1 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let reactor = self.reactor else { return .init(frame: .zero) } - - if reactor.currentState.isDisplayedCardsEmpty { - - return self.cellForPlaceholder(tableView, for: indexPath) - } else { - - return self.cellForMainHome(tableView, for: indexPath, with: reactor) - } - } -} - -extension MainHomeLatestViewController: UITableViewDataSourcePrefetching { - - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - guard let reactor = self.reactor else { return } - - indexPaths.forEach { indexPath in - // 데이터 로드 전, 이미지 캐싱 - let strUrl = reactor.currentState.displayedCards[indexPath.row].backgroundImgURL.url - KingfisherManager.shared.download(strUrl: strUrl) { _ in } - } - } -} - -extension MainHomeLatestViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let reactor = self.reactor else { return } - - let selectedId = reactor.currentState.displayedCards[indexPath.row].id - self.willPushCardId.accept(selectedId) - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return (self.reactor?.currentState.isDisplayedCardsEmpty ?? true) ? tableView.bounds.height : self.cellHeight - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard self.reactor?.currentState.isDisplayedCardsEmpty == false else { return } - - let lastSectionIndex = tableView.numberOfSections - 1 - let lastRowIndex = tableView.numberOfRows(inSection: lastSectionIndex) - 1 - - if self.isLoadingMore, - indexPath.section == lastSectionIndex, - indexPath.row == lastRowIndex, - let reactor = self.reactor { - - let lastId = reactor.currentState.displayedCards[indexPath.row].id - reactor.action.onNext(.moreFind(lastId)) - } - } - - - // MARK: UIScrollView Delegate - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - - // currentOffset <= 0 && isLoading == false 일 때, 테이블 뷰 새로고침 가능 - self.isRefreshEnabled = (self.currentOffset <= 0 && self.reactor?.currentState.isLoading == false) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - - let offset = scrollView.contentOffset.y - - // 당겨서 새로고침 상황일 때 - guard offset > 0 else { - - self.hidesHeaderContainer.accept(false) - self.currentOffset = offset - self.moveTopButton.isHidden = true - - return - } - - // 정상적인 스크롤 상황일 때, 헤더뷰 숨김 - guard offset <= (scrollView.contentSize.height - scrollView.frame.height) else { return } - - // offset이 currentOffset보다 크면 아래로 스크롤, 반대일 경우 위로 스크롤 - // 위로 스크롤 중일 때 헤더뷰 표시, 아래로 스크롤 중일 때 헤더뷰 숨김 - self.hidesHeaderContainer.accept(offset > self.currentOffset) - - // 아래로 스크롤 중일 때, 데이터 추가로드 가능 - self.isLoadingMore = offset > self.currentOffset - - self.currentOffset = offset - - // 최상단일 때만 moveToButton 숨김 - self.moveTopButton.isHidden = self.currentOffset <= 0 - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - - let offset = scrollView.contentOffset.y - - // isRefreshEnabled == true 이고, 스크롤이 끝났을 경우에만 테이블 뷰 새로고침 - if self.isRefreshEnabled, - let refreshControl = self.tableView.refreshControl, - offset <= -(refreshControl.frame.origin.y + 40) { - - refreshControl.beginRefreshingFromTop() - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Latest/MainHomeLatestViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Latest/MainHomeLatestViewReactor.swift deleted file mode 100644 index 3a829f2a..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Latest/MainHomeLatestViewReactor.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// MainHomeLatestViewReactor.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import ReactorKit - - -class MainHomeLatestViewReactor: Reactor { - - // hasMoreUpdate == true 일 때, moreFind - typealias CardsWithUpdate = (cards: [Card], hasMoreUpdate: Bool) - - enum Action: Equatable { - case landing - case refresh - case moreFind(String) - } - - enum Mutation { - case cards(CardsWithUpdate) - case more(CardsWithUpdate) - case updateIsLoading(Bool) - case updateIsProcessing(Bool) - } - - struct State { - fileprivate(set) var displayedCardsWithUpdate: CardsWithUpdate? - fileprivate(set) var isLoading: Bool - fileprivate(set) var isProcessing: Bool - - var displayedCards: [Card] { - return self.displayedCardsWithUpdate?.cards ?? [] - } - var isDisplayedCardsEmpty: Bool { - return self.displayedCards.isEmpty - } - var displayedCardsCount: Int { - return self.isDisplayedCardsEmpty ? 1 : self.displayedCards.count - } - } - - var initialState: State = .init( - displayedCardsWithUpdate: nil, - isLoading: false, - isProcessing: false - ) - - let provider: ManagerProviderType - - // TODO: 페이징 - // private let countPerLoading: Int = 10 - - init(provider: ManagerProviderType) { - self.provider = provider - } - - - func mutate(action: Action) -> Observable { - switch action { - case .landing: - - return .concat([ - .just(.updateIsProcessing(true)), - self.refresh() - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsProcessing(false)) - ]) - case .refresh: - - return .concat([ - .just(.updateIsLoading(true)), - self.refresh() - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsLoading(false)) - ]) - case let .moreFind(lastId): - - return .concat([ - .just(.updateIsProcessing(true)), - self.moreFind(lastId) - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsProcessing(false)) - ]) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var state: State = state - switch mutation { - case let .cards(displayedCardsWithUpdate): - state.displayedCardsWithUpdate = displayedCardsWithUpdate - case let .more(displayedCardsWithUpdate): - state.displayedCardsWithUpdate?.cards += displayedCardsWithUpdate.cards - state.displayedCardsWithUpdate?.hasMoreUpdate = displayedCardsWithUpdate.hasMoreUpdate - case let .updateIsLoading(isLoading): - state.isLoading = isLoading - case let .updateIsProcessing(isProcessing): - state.isProcessing = isProcessing - } - return state - } -} - -extension MainHomeLatestViewReactor { - - func refresh() -> Observable { - - let latitude = self.provider.locationManager.coordinate.latitude - let longitude = self.provider.locationManager.coordinate.longitude - - let request: CardRequest = .latestCard(lastId: nil, latitude: latitude, longitude: longitude) - return self.provider.networkManager.request(LatestCardResponse.self, request: request) - .map(\.embedded.cards) - .map { Mutation.cards((cards: $0, hasMoreUpdate: false)) } - .catch(self.catchClosure) - } - - func moreFind(_ lastId: String) -> Observable { - - let latitude = self.provider.locationManager.coordinate.latitude - let longitude = self.provider.locationManager.coordinate.longitude - - let request: CardRequest = .latestCard(lastId: lastId, latitude: latitude, longitude: longitude) - return self.provider.networkManager.request(LatestCardResponse.self, request: request) - .map(\.embedded.cards) - .map { Mutation.more((cards: $0, hasMoreUpdate: true)) } - .catch(self.catchClosure) - } -} - -extension MainHomeLatestViewReactor { - - var catchClosure: ((Error) throws -> Observable ) { - return { _ in - .concat([ - .just(.cards((cards: [], hasMoreUpdate: false))), - .just(.updateIsProcessing(false)), - .just(.updateIsLoading(false)) - ]) - } - } - - // TODO: 페이징 - // func separate(displayed displayedCards: [Card], current cards: [Card]) -> [Card] { - // let count = displayedCards.count - // let displayedCards = Array(cards[count.. Bool { - return prevCardsWithUpdate.cards == currCardsWithUpdate.cards && - prevCardsWithUpdate.hasMoreUpdate == currCardsWithUpdate.hasMoreUpdate - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/MainHomeTabBarController.swift b/SOOUM/SOOUM/Presentations/Main/Home/MainHomeTabBarController.swift deleted file mode 100644 index 73ad9e7e..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/MainHomeTabBarController.swift +++ /dev/null @@ -1,432 +0,0 @@ -// -// MainHomeTabBarController.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import UIKit - -import SnapKit -import Then - -import ReactorKit -import RxCocoa -import RxSwift - - -class MainHomeTabBarController: BaseNavigationViewController, View { - - enum Text { - static let tabLatestTitle: String = "최신순" - static let tabPopularityTitle: String = "인기순" - static let tabDistanceTitle: String = "거리순" - - static let dialogTitle: String = "위치 정보 사용 설정" - static let dialogMessage: String = "위치 확인을 위해 권한 설정이 필요해요" - - static let cancelActionTitle: String = "취소" - static let settingActionTitle: String = "설정" - } - - - // MARK: Set navigationBar Items - - private let logo = UIImageView().then { - $0.image = .init(.logo(.logo)) - $0.tintColor = .som.p300 - $0.contentMode = .scaleAspectFit - } - - private let rightAlamButton = SOMButton().then { - $0.image = .init(.icon(.outlined(.alarm))) - $0.foregroundColor = .som.gray700 - } - - private let dotWithoutReadView = UIView().then { - $0.backgroundColor = .som.red - $0.layer.cornerRadius = 6 * 0.5 - $0.clipsToBounds = true - $0.isHidden = true - } - - - // MARK: Views - - private let headerContainer = UIStackView().then { - $0.backgroundColor = .som.white - $0.axis = .vertical - } - - private lazy var headerTapBar = SOMSwipeTabBar(alignment: .left).then { - $0.items = [Text.tabLatestTitle, Text.tabPopularityTitle, Text.tabDistanceTitle] - - $0.delegate = self - } - - private lazy var headerLocationFilter = SOMLocationFilter().then { - $0.delegate = self - } - - private lazy var pageViewController = UIPageViewController( - transitionStyle: .scroll, - navigationOrientation: .horizontal - ).then { - $0.dataSource = self - $0.delegate = self - } - - - // MARK: Variables - - private var pages = [UIViewController]() - private var currentPage: Int = 0 - - private var animator: UIViewPropertyAnimator? - - private var locationFilterHeight: CGFloat = 0 - - - // MARK: Constraints - - private var headerTapBarHeightConstraint: Constraint? - private var headerLocationFilterHeightConstraint: Constraint? - - - // MARK: Override func - - override func setupNaviBar() { - super.setupNaviBar() - - self.navigationBar.titleView = self.logo - self.navigationBar.titlePosition = .left - - self.navigationBar.hidesBackButton = true - - self.rightAlamButton.snp.makeConstraints { - $0.size.equalTo(24) - } - (self.rightAlamButton.imageView ?? self.rightAlamButton).addSubview(self.dotWithoutReadView) - self.dotWithoutReadView.snp.makeConstraints { - $0.top.equalToSuperview().offset(2) - $0.trailing.equalToSuperview().offset(-3) - $0.size.equalTo(6) - } - self.navigationBar.setRightButtons([self.rightAlamButton]) - } - - override func bind() { - super.bind() - - // 탭바 표시 - self.rx.viewWillAppear - .subscribe(with: self) { object, _ in - object.hidesBottomBarWhenPushed = false - } - .disposed(by: self.disposeBag) - - // 알림 화면으로 전환 - self.rightAlamButton.rx.tap - .subscribe(with: self) { object, _ in - let notificatinoTabBarController = NotificationTabBarController() - notificatinoTabBarController.reactor = object.reactor?.reactorForNoti() - object.navigationPush(notificatinoTabBarController, animated: true, bottomBarHidden: true) - } - .disposed(by: self.disposeBag) - - // 유저 정보 모두 제거 후 온보딩 화면으로 전환 - #if DEVELOP - logo.rx.longPressGesture() - .when(.began) - .subscribe(with: self) { object, _ in - AuthKeyChain.shared.delete(.deviceId) - AuthKeyChain.shared.delete(.refreshToken) - AuthKeyChain.shared.delete(.accessToken) - - DispatchQueue.main.async { - if let windowScene: UIWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window: UIWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { - - // let viewController = OnboardingViewController() - // viewController.reactor = OnboardingViewReactor(provider: object.reactor!.provider) - // window.rootViewController = UINavigationController(rootViewController: viewController) - } - } - } - .disposed(by: self.disposeBag) - #endif - } - - override func setupConstraints() { - super.setupConstraints() - - self.addChild(self.pageViewController) - self.view.addSubview(self.pageViewController.view) - self.pageViewController.view.snp.makeConstraints { - $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) - $0.bottom.leading.trailing.equalToSuperview() - } - - self.view.addSubview(self.headerContainer) - self.headerContainer.snp.makeConstraints { - $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) - $0.leading.trailing.equalToSuperview() - } - self.headerContainer.addArrangedSubview(self.headerTapBar) - self.headerTapBar.snp.makeConstraints { - self.headerTapBarHeightConstraint = $0.height.equalTo(SOMSwipeTabBar.Height.mainHome).priority(.high).constraint - } - self.headerContainer.addArrangedSubview(self.headerLocationFilter) - self.headerLocationFilter.snp.makeConstraints { - self.headerLocationFilterHeightConstraint = $0.height.equalTo(0).priority(.high).constraint - } - } - - - // MARK: ReactorKit - bind - - func bind(reactor: MainHomeTabBarReactor) { - - let mainHomeLatestViewController = MainHomeLatestViewController() - mainHomeLatestViewController.reactor = reactor.reactorForLatest() - - self.pages.append(mainHomeLatestViewController) - - let mainHomePopularViewController = MainHomePopularViewController() - mainHomePopularViewController.reactor = reactor.reactorForPopular() - - self.pages.append(mainHomePopularViewController) - - let mainHomeDistanceViewController = MainHomeDistanceViewController() - mainHomeDistanceViewController.reactor = reactor.reactorForDistance() - - self.pages.append(mainHomeDistanceViewController) - - self.currentPage = 0 - self.pageViewController.setViewControllers( - [self.pages[0]], - direction: .forward, - animated: false, - completion: nil - ) - - // 각 뷰컨트롤러의 hidesHeaderContainer 구독 - Observable.merge( - mainHomeLatestViewController.hidesHeaderContainer.distinctUntilChanged().asObservable(), - mainHomePopularViewController.hidesHeaderContainer.distinctUntilChanged().asObservable(), - mainHomeDistanceViewController.hidesHeaderContainer.distinctUntilChanged().asObservable() - ) - .observe(on: MainScheduler.instance) - .subscribe(with: self) { object, hidesHeaderContainer in - - // 애니메이터가 이미 실행 중이라면 취소하고 새 애니메이션 시작 - if object.animator?.state == .active { - - object.animator?.stopAnimation(false) - object.animator?.finishAnimation(at: .end) - } - // 헤더 뷰 높이 조절 - object.headerTapBarHeightConstraint?.deactivate() - object.headerLocationFilterHeightConstraint?.deactivate() - object.headerTapBar.snp.makeConstraints { - let height = hidesHeaderContainer ? 0 : SOMSwipeTabBar.Height.mainHome - object.headerTapBarHeightConstraint = $0.height.equalTo(height).priority(.high).constraint - } - object.headerLocationFilter.snp.makeConstraints { - let height = hidesHeaderContainer ? 0 : object.locationFilterHeight - object.headerLocationFilterHeightConstraint = $0.height.equalTo(height).priority(.high).constraint - } - - // 애니메이션 추가 - object.animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) - object.animator?.addAnimations { - - object.view.layoutIfNeeded() - } - // 새 애니메이션 시작 - object.animator?.startAnimation() - object.animator?.addCompletion { position in - // 애니메이션이 끝난 후 animator 초기화 - if position == .end { object.animator = nil } - // Update headerContainer hidden - object.headerContainer.isHidden = hidesHeaderContainer - } - } - .disposed(by: self.disposeBag) - - // 각 뷰컨트롤러의 willPushCardId 구독 - Observable.merge( - mainHomeLatestViewController.willPushCardId.asObservable(), - mainHomePopularViewController.willPushCardId.asObservable(), - mainHomeDistanceViewController.willPushCardId.asObservable() - ) - .subscribe(with: self) { object, willPushCardId in - - let detailViewController = DetailViewController() - detailViewController.reactor = reactor.reactorForDetail(willPushCardId) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) - } - .disposed(by: self.disposeBag) - - // Action - self.rx.viewWillAppear - .map { _ in Reactor.Action.notisWithoutRead } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - // State - reactor.state.map(\.noNotisWithoutRead) - .bind(to: self.dotWithoutReadView.rx.isHidden) - .disposed(by: self.disposeBag) - } -} - - -// MARK: Private func - -extension MainHomeTabBarController { - - private func showLocationPermissionDialog() { - - let cancelAction = SOMDialogAction( - title: Text.cancelActionTitle, - style: .gray, - action: { - UIApplication.topViewController?.dismiss(animated: true) - } - ) - let settingAction = SOMDialogAction( - title: Text.settingActionTitle, - style: .primary, - action: { - let application = UIApplication.shared - let openSettingsURLString: String = UIApplication.openSettingsURLString - if let settingsURL = URL(string: openSettingsURLString), - application.canOpenURL(settingsURL) { - application.open(settingsURL) - } - - UIApplication.topViewController?.dismiss(animated: true) - } - ) - - SOMDialogViewController.show( - title: Text.dialogTitle, - message: Text.dialogMessage, - actions: [cancelAction, settingAction] - ) - } -} - - -// MARK: SOMSwipeTabBarDelegate - -extension MainHomeTabBarController: SOMSwipeTabBarDelegate { - - func tabBar(_ tabBar: SOMSwipeTabBar, shouldSelectTabAt index: Int) -> Bool { - - if index == 2, self.reactor?.provider.locationManager.checkLocationAuthStatus() == .denied { - - self.showLocationPermissionDialog() - return false - } - - return true - } - - func tabBar(_ tabBar: SOMSwipeTabBar, didSelectTabAt index: Int) { - - let hidesLocationFilter = index != 2 - - self.locationFilterHeight = hidesLocationFilter ? 0 : SOMLocationFilter.height - - self.headerLocationFilterHeightConstraint?.deactivate() - self.headerLocationFilter.snp.makeConstraints { - self.headerLocationFilterHeightConstraint = $0.height.equalTo(self.locationFilterHeight).priority(.high).constraint - } - - UIView.performWithoutAnimation { - self.view.layoutIfNeeded() - } - - if self.currentPage != index { - - self.currentPage = index - self.pageViewController.setViewControllers( - [self.pages[index]], - direction: tabBar.previousIndex <= index ? .forward : .reverse, - animated: true, - completion: nil - ) - } - } -} - - -// MARK: SOMLocationFilterDelegate - -extension MainHomeTabBarController: SOMLocationFilterDelegate { - - func filter(_ filter: SOMLocationFilter, didSelectDistanceAt distance: SOMLocationFilter.Distance) { - guard filter.prevDistance != distance, - let mainHomeDistanceViewController = self.pages[self.currentPage] as? MainHomeDistanceViewController - else { return } - - mainHomeDistanceViewController.reactor?.action.onNext(.distanceFilter(distance.rawValue)) - } -} - - -// MARK: UIPageViewController dataSource and delegate - -extension MainHomeTabBarController: UIPageViewControllerDataSource { - - func pageViewController( - _ pageViewController: UIPageViewController, - viewControllerBefore viewController: UIViewController - ) -> UIViewController? { - guard let currentIndex = self.pages.firstIndex(of: viewController), - currentIndex > 0 - else { return nil } - - return self.pages[currentIndex - 1] - } - - func pageViewController( - _ pageViewController: UIPageViewController, - viewControllerAfter viewController: UIViewController - ) -> UIViewController? { - guard let currentIndex = self.pages.firstIndex(of: viewController), - currentIndex < self.pages.count - 1 - else { return nil } - - // TODO: 임시, 위치 권한 허용 X일 때, 거리순 탭으로 진입 시 스와이프 제스처 막음 - if currentIndex == 1, - self.reactor?.provider.locationManager.checkLocationAuthStatus() == .denied { - return nil - } else { - return self.pages[currentIndex + 1] - } - } -} - -extension MainHomeTabBarController: UIPageViewControllerDelegate { - - func pageViewController( - _ pageViewController: UIPageViewController, - willTransitionTo pendingViewControllers: [UIViewController] - ) { - self.currentPage = self.pages.firstIndex(of: pendingViewControllers[0]) ?? 0 - } - - func pageViewController( - _ pageViewController: UIPageViewController, - didFinishAnimating finished: Bool, - previousViewControllers: [UIViewController], - transitionCompleted completed: Bool - ) { - if completed { - self.headerTapBar.didSelectTabBarItem(self.currentPage) - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/MainHomeTabBarReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/MainHomeTabBarReactor.swift deleted file mode 100644 index e6910fa1..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/MainHomeTabBarReactor.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// MainHomeTabBarReactor.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import ReactorKit - -import Alamofire - - -class MainHomeTabBarReactor: Reactor { - - enum Action: Equatable { - case notisWithoutRead - case requestRead(String) - } - - - enum Mutation { - case notisWithoutRead(Bool) - case updateIsReadCompleted(Bool) - } - - struct State { - var noNotisWithoutRead: Bool - var isReadCompleted: Bool - } - - var initialState: State = .init( - noNotisWithoutRead: true, - isReadCompleted: false - ) - - let provider: ManagerProviderType - - init(provider: ManagerProviderType) { - self.provider = provider - } - - func mutate(action: Action) -> Observable { - switch action { - case .notisWithoutRead: - // let request: NotificationRequest = .totalWithoutReadCount - // - // return self.provider.networkManager.request(WithoutReadNotisCountResponse.self, request: request) - // .map { $0.unreadCnt == "0" } - // .map(Mutation.notisWithoutRead) - return .empty() - case let .requestRead(selectedId): - let request: NotificationRequest = .requestRead(notificationId: selectedId) - return self.provider.networkManager.request(Empty.self, request: request) - .map { _ in .updateIsReadCompleted(true) } - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var state = state - switch mutation { - case let .notisWithoutRead(noNotisWithoutRead): - state.noNotisWithoutRead = noNotisWithoutRead - case let .updateIsReadCompleted(isReadCompleted): - state.isReadCompleted = isReadCompleted - } - return state - } -} - -extension MainHomeTabBarReactor { - - func reactorForLatest() -> MainHomeLatestViewReactor { - MainHomeLatestViewReactor(provider: self.provider) - } - - func reactorForPopular() -> MainHomePopularViewReactor { - MainHomePopularViewReactor(provider: self.provider) - } - - func reactorForDistance() -> MainHomeDistanceViewReactor { - MainHomeDistanceViewReactor(provider: self.provider) - } - - func reactorForDetail(_ selectedId: String) -> DetailViewReactor { - DetailViewReactor.init(provider: self.provider, selectedId) - } - - func reactorForNoti() -> NotificationTabBarReactor { - NotificationTabBarReactor(provider: self.provider) - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationTabBarController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationTabBarController.swift deleted file mode 100644 index 7bcfd2d8..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationTabBarController.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// NotificationTabBarController.swift -// SOOUM -// -// Created by 오현식 on 12/20/24. -// - -import UIKit - -import SnapKit -import Then - -import ReactorKit -import RxCocoa -import RxSwift - - -class NotificationTabBarController: BaseNavigationViewController, View { - - enum Text { - static let navigationTitle: String = "알림" - - static let tabTotalTitle: String = "전체" - static let tabCommentTitle: String = "답카드" - static let tabLikeTitle: String = "공감" - } - - - // MARK: Views - - private lazy var headerTabBar = SOMSwipeTabBar(alignment: .fill).then { - $0.inset = .zero - $0.spacing = 0 - $0.seperatorHeight = 1.4 - $0.seperatorColor = .som.gray300 - $0.items = [Text.tabTotalTitle, Text.tabCommentTitle, Text.tabLikeTitle] - - $0.delegate = self - } - - private lazy var pageViewController = UIPageViewController( - transitionStyle: .scroll, - navigationOrientation: .horizontal - ).then { - $0.dataSource = self - $0.delegate = self - } - - - // MARK: Variables - - private var pages = [UIViewController]() - private var currentPage: Int = 0 - - - // MARK: Override variables - - override var navigationBarHeight: CGFloat { - 46 - } - - - // MARK: Override func - - override func setupNaviBar() { - super.setupNaviBar() - - self.navigationBar.title = Text.navigationTitle - } - - override func setupConstraints() { - super.setupConstraints() - - self.view.addSubview(self.headerTabBar) - self.headerTabBar.snp.makeConstraints { - $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) - $0.leading.trailing.equalToSuperview() - $0.height.equalTo(SOMSwipeTabBar.Height.notification) - } - - self.addChild(self.pageViewController) - self.view.addSubview(self.pageViewController.view) - self.pageViewController.view.snp.makeConstraints { - $0.top.equalTo(self.headerTabBar.snp.bottom) - $0.bottom.leading.trailing.equalToSuperview() - } - } - - - // MARK: ReactorKit - bind - - func bind(reactor: NotificationTabBarReactor) { - - let notificationTotalViewController = NotificationViewController() - notificationTotalViewController.reactor = reactor.reactorForTotal() - - self.pages.append(notificationTotalViewController) - - let notificationCommentViewController = NotificationViewController() - notificationCommentViewController.reactor = reactor.reactorForComment() - - self.pages.append(notificationCommentViewController) - - let notificationLikeViewController = NotificationViewController() - notificationLikeViewController.reactor = reactor.reactorForLike() - - self.pages.append(notificationLikeViewController) - - self.currentPage = 0 - self.pageViewController.setViewControllers( - [self.pages[0]], - direction: .forward, - animated: true, - completion: nil - ) - - // 각 뷰컨트롤러의 willPushCardId 구독 - Observable.merge( - notificationTotalViewController.willPushCardId.asObservable(), - notificationCommentViewController.willPushCardId.asObservable(), - notificationLikeViewController.willPushCardId.asObservable() - ) - .subscribe(with: self) { object, willPushCardId in - - let detailViewController = DetailViewController() - detailViewController.reactor = reactor.reactorForDetail(willPushCardId) - object.navigationPush(detailViewController, animated: true, bottomBarHidden: true) - } - .disposed(by: self.disposeBag) - } -} - -extension NotificationTabBarController: SOMSwipeTabBarDelegate { - - func tabBar(_ tabBar: SOMSwipeTabBar, shouldSelectTabAt index: Int) -> Bool { - return true - } - - func tabBar(_ tabBar: SOMSwipeTabBar, didSelectTabAt index: Int) { - - if self.currentPage != index { - - self.currentPage = index - self.pageViewController.setViewControllers( - [self.pages[index]], - direction: tabBar.previousIndex <= index ? .forward : .reverse, - animated: true, - completion: nil - ) - } - } -} - -extension NotificationTabBarController: UIPageViewControllerDataSource { - - func pageViewController( - _ pageViewController: UIPageViewController, - viewControllerBefore viewController: UIViewController - ) -> UIViewController? { - guard let currentIndex = self.pages.firstIndex(of: viewController), - currentIndex > 0 - else { return nil } - - return self.pages[currentIndex - 1] - } - - func pageViewController( - _ pageViewController: UIPageViewController, - viewControllerAfter viewController: UIViewController - ) -> UIViewController? { - guard let currentIndex = self.pages.firstIndex(of: viewController), - currentIndex < self.pages.count - 1 - else { return nil } - - return self.pages[currentIndex + 1] - } -} - -extension NotificationTabBarController: UIPageViewControllerDelegate { - - func pageViewController( - _ pageViewController: UIPageViewController, - willTransitionTo pendingViewControllers: [UIViewController] - ) { - self.currentPage = self.pages.firstIndex(of: pendingViewControllers[0]) ?? 0 - } - - func pageViewController( - _ pageViewController: UIPageViewController, - didFinishAnimating finished: Bool, - previousViewControllers: [UIViewController], - transitionCompleted completed: Bool - ) { - if completed { - self.headerTabBar.didSelectTabBarItem(self.currentPage) - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationTabBarReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationTabBarReactor.swift deleted file mode 100644 index a9484931..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationTabBarReactor.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// NotificationTabBarReactor.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import ReactorKit - - -class NotificationTabBarReactor: Reactor { - - typealias Action = NoAction - typealias Mutation = NoMutation - - struct State { } - - var initialState: State { .init() } - - let provider: ManagerProviderType - - init(provider: ManagerProviderType) { - self.provider = provider - } -} - -extension NotificationTabBarReactor { - - func reactorForTotal() -> NotificationViewReactor { - NotificationViewReactor(provider: self.provider, .total) - } - - func reactorForComment() -> NotificationViewReactor { - NotificationViewReactor(provider: self.provider, .comment) - } - - func reactorForLike() -> NotificationViewReactor { - NotificationViewReactor(provider: self.provider, .like) - } - - func reactorForDetail(_ selectedId: String) -> DetailViewReactor { - DetailViewReactor(provider: self.provider, selectedId) - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift new file mode 100644 index 00000000..80ebdd01 --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewController.swift @@ -0,0 +1,493 @@ +// +// NotificationViewController.swift +// SOOUM +// +// Created by 오현식 on 12/23/24. +// + +import UIKit + +import SnapKit +import Then + +import ReactorKit +import RxCocoa +import RxSwift + + +class NotificationViewController: BaseNavigationViewController, View { + + enum Text { + static let navigationTitle: String = "알림" + + static let activityTitle: String = "활동" + static let noticeTitle: String = "공지사항" + + static let headerTextForRead: String = "지난 알림" + } + + enum Section: Int, CaseIterable { + case unread + case read + case notice + case empty + } + + enum Item: Hashable { + case unread(CompositeNotificationInfo) + case read(CompositeNotificationInfo) + case notice(NoticeInfo) + case empty + } + + + // MARK: Views + + private lazy var headerView = SOMSwipableTabBar().then { + $0.items = [Text.activityTitle, Text.noticeTitle] + $0.delegate = self + } + + private lazy var tableView = UITableView().then { + $0.backgroundColor = .clear + $0.indicatorStyle = .black + $0.separatorStyle = .none + + $0.isHidden = true + + $0.sectionHeaderTopPadding = .zero + $0.decelerationRate = .fast + + $0.refreshControl = SOMRefreshControl() + + $0.register( + NotificationViewCell.self, + forCellReuseIdentifier: NotificationViewCell.cellIdentifier + ) + $0.register( + NoticeViewCell.self, + forCellReuseIdentifier: NoticeViewCell.cellIdentifier + ) + $0.register( + NotificationPlaceholderViewCell.self, + forCellReuseIdentifier: NotificationPlaceholderViewCell.cellIdentifier + ) + + $0.delegate = self + } + + + // MARK: Variables + + typealias DataSource = UITableViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + + private lazy var dataSource = DataSource(tableView: self.tableView) { [weak self] tableView, indexPath, item -> UITableViewCell? in + + guard let self = self else { return nil } + + switch item { + case let .unread(notification): + + let cell: NotificationViewCell = self.cellForNotification(tableView, with: indexPath) + cell.bind(notification, isReaded: false) + + return cell + case let .read(notification): + + let cell: NotificationViewCell = self.cellForNotification(tableView, with: indexPath) + cell.bind(notification, isReaded: true) + + return cell + case let .notice(notice): + + let cell: NoticeViewCell = self.cellForNotice(tableView, with: indexPath) + cell.bind(notice) + + return cell + case .empty: + + return self.cellForPlaceholder(tableView, with: indexPath) + } + } + + private var initialOffset: CGFloat = 0 + private var currentOffset: CGFloat = 0 + private var isRefreshEnabled: Bool = true + private var shouldRefreshing: Bool = false + + + // MARK: Variables + Rx + + let willPushCardId = PublishRelay() + + + // MARK: Override func + + override func setupNaviBar() { + super.setupNaviBar() + + self.navigationBar.title = Text.navigationTitle + } + + override func setupConstraints() { + super.setupConstraints() + + self.view.addSubview(self.headerView) + self.headerView.snp.makeConstraints { + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) + $0.leading.trailing.equalToSuperview() + } + + self.view.addSubview(self.tableView) + self.tableView.snp.makeConstraints { + $0.top.equalTo(self.headerView.snp.bottom) + $0.bottom.horizontalEdges.equalToSuperview() + } + } + + + // MARK: ReactorKit - bind + + func bind(reactor: NotificationViewReactor) { + + // Action + self.rx.viewDidLoad + .map { _ in Reactor.Action.landing } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + let isRefreshing = reactor.state.map(\.isRefreshing).distinctUntilChanged().share() + self.tableView.refreshControl?.rx.controlEvent(.valueChanged) + .withLatestFrom(isRefreshing) + .filter { $0 == false } + .delay(.milliseconds(1000), scheduler: MainScheduler.instance) + .map { _ in Reactor.Action.refresh } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + // State + isRefreshing + .observe(on: MainScheduler.asyncInstance) + .filter { $0 == false } + .subscribe(with: self.tableView) { tableView, _ in + tableView.refreshControl?.endRefreshing() + } + .disposed(by: self.disposeBag) + + reactor.state.map(\.displayType) + .filter { $0 == .notice } + .take(1) + .subscribe(with: self.headerView) { headerView, _ in + headerView.didSelectTabBarItem(1, onlyUpdateApperance: true) + } + .disposed(by: self.disposeBag) + + reactor.state.map { + NotificationViewReactor.DisplayStates( + displayType: $0.displayType, + unreads: $0.notificationsForUnread, + reads: $0.notifications, + notices: $0.notices + ) + } + .observe(on: MainScheduler.instance) + .subscribe(with: self) { object, displayStats in + + var snapshot = Snapshot() + snapshot.appendSections(Section.allCases) + + switch displayStats.displayType { + case .activity: + + guard let unreads = displayStats.unreads, let reads = displayStats.reads else { return } + + guard unreads.isEmpty == false, reads.isEmpty == false else { + snapshot.appendItems([.empty], toSection: .empty) + break + } + + let newUnreads = unreads.map { Item.unread($0) } + snapshot.appendItems(newUnreads, toSection: .unread) + + let newReads = reads.map { Item.read($0) } + snapshot.appendItems(newReads, toSection: .read) + case .notice: + + guard let notices = displayStats.notices else { return } + + guard notices.isEmpty == false else { + snapshot.appendItems([.empty], toSection: .empty) + break + } + + let new = notices.map { Item.notice($0) } + snapshot.appendItems(new, toSection: .notice) + } + + object.dataSource.apply(snapshot, animatingDifferences: false) + + object.tableView.isHidden = false + } + .disposed(by: self.disposeBag) + } +} + + +// MARK: Cells + +private extension NotificationViewController { + + func cellForPlaceholder( + _ tableView: UITableView, + with indexPath: IndexPath + ) -> NotificationPlaceholderViewCell { + + return tableView.dequeueReusableCell( + withIdentifier: NotificationPlaceholderViewCell.cellIdentifier, + for: indexPath + ) as! NotificationPlaceholderViewCell + } + + func cellForNotification( + _ tableView: UITableView, + with indexPath: IndexPath + ) -> NotificationViewCell { + + return tableView.dequeueReusableCell( + withIdentifier: NotificationViewCell.cellIdentifier, + for: indexPath + ) as! NotificationViewCell + } + + func cellForNotice( + _ tableView: UITableView, + with indexPath: IndexPath + ) -> NoticeViewCell { + + return tableView.dequeueReusableCell( + withIdentifier: NoticeViewCell.cellIdentifier, + for: indexPath + ) as! NoticeViewCell + } +} + + +// MARK: UITableViewDelegate + +extension NotificationViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + // TODO: 상세보기 or 공지사항 화면 전환 필요 + // switch Section.allCases[indexPath.section] { + // case .withoutRead: + // let selectedId = self.notificationsWithoutRead[indexPath.row].id + // + // self.reactor?.action.onNext(.requestRead("\(selectedId)")) + // let targetCardId = self.notificationsWithoutRead[indexPath.row].targetCardId + // self.willPushCardId.accept("\(targetCardId ?? 0)") + // case .read: + // let targetCardId = self.notifications[indexPath.row].targetCardId + // self.willPushCardId.accept("\(targetCardId ?? 0)") + // case .empty: + // break + // } + guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return } + + switch item { + case let .notice(notice): + + if let url = URL(string: notice.url), + UIApplication.shared.canOpenURL(url) { + + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + case let .unread(notification): + // TODO: 상세보기 화면 전환 + return + case let .read(notification): + // TODO: 상세보기 화면 전환 + return + default: + return + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + + let sections = self.dataSource.snapshot().sectionIdentifiers + guard sections.isEmpty == false else { return nil } + + switch sections[section] { + case .read: + + let backgroundView = UIView().then { + $0.backgroundColor = .som.v2.white + } + + let label = UILabel().then { + $0.text = Text.headerTextForRead + $0.textColor = .som.v2.black + + let typography = Typography.som.v2.subtitle3.withAlignment(.left) + $0.typography = typography + + let frame = CGRect( + x: 25, + y: 32, + width: UIScreen.main.bounds.width, + height: typography.lineHeight + ) + $0.frame = frame + } + backgroundView.addSubview(label) + + return backgroundView + default: + + return nil + } + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + + let sections = self.dataSource.snapshot().sectionIdentifiers + guard sections.isEmpty == false else { return 0 } + + switch sections[section] { + case .read: + + return (self.reactor?.currentState.notifications?.isEmpty ?? true) ? 0 : 53 + default: + + return 0 + } + } + + func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + + guard let reactor = self.reactor, reactor.currentState.isRefreshing == false else { return } + + switch reactor.currentState.displayType { + case let .activity(activityType): + + switch activityType { + case .unread: + + let lastRowIndexPath = tableView.numberOfRows(inSection: Section.unread.rawValue) - 1 + if reactor.currentState.notificationsForUnread?.isEmpty == false, + indexPath.section == Section.unread.rawValue, + indexPath.row == lastRowIndexPath { + + var lastId: String { + switch reactor.currentState.notificationsForUnread?.last { + case let .default(notification): + return notification.notificationInfo.notificationId + case let .follow(notification): + return notification.notificationInfo.notificationId + case let .deleted(notification): + return notification.notificationInfo.notificationId + case let .blocked(notification): + return notification.notificationInfo.notificationId + default: + return "" + } + } + reactor.action.onNext(.moreFind(lastId: lastId, displayType: .activity(.unread))) + } + case .read: + + let lastRowIndexPath = tableView.numberOfRows(inSection: Section.read.rawValue) - 1 + if reactor.currentState.notifications?.isEmpty == false, + indexPath.section == Section.read.rawValue, + indexPath.row == lastRowIndexPath { + + var lastId: String { + switch reactor.currentState.notificationsForUnread?.last { + case let .default(notification): + return notification.notificationInfo.notificationId + case let .follow(notification): + return notification.notificationInfo.notificationId + case let .deleted(notification): + return notification.notificationInfo.notificationId + case let .blocked(notification): + return notification.notificationInfo.notificationId + default: + return "" + } + } + reactor.action.onNext(.moreFind(lastId: lastId, displayType: .activity(.read))) + } + } + case .notice: + + let lastRowIndexPath = tableView.numberOfRows(inSection: Section.notice.rawValue) - 1 + if reactor.currentState.notices?.isEmpty == false, + indexPath.section == Section.notice.rawValue, + indexPath.row == lastRowIndexPath { + + let lastId = reactor.currentState.notices?.last?.id ?? "" + reactor.action.onNext(.moreFind(lastId: lastId, displayType: .notice)) + } + } + } + + + // MARK: UIScrollViewDelegate + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + + let offset = scrollView.contentOffset.y + + // currentOffset <= 0 && isLoading == false 일 때, 테이블 뷰 새로고침 가능 + self.isRefreshEnabled = (offset <= 0) && (self.reactor?.currentState.isRefreshing == false) + self.shouldRefreshing = false + self.initialOffset = offset + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + + let offset = scrollView.contentOffset.y + + // 당겨서 새로고침 + if self.isRefreshEnabled, offset < self.initialOffset { + guard let refreshControl = self.tableView.refreshControl else { + self.currentOffset = offset + return + } + + let pulledOffset = self.initialOffset - offset + let refreshingOffset = refreshControl.frame.origin.y + refreshControl.frame.height + self.shouldRefreshing = abs(pulledOffset) >= refreshingOffset + } + + self.currentOffset = offset + } + + func scrollViewWillEndDragging( + _ scrollView: UIScrollView, + withVelocity velocity: CGPoint, + targetContentOffset: UnsafeMutablePointer + ) { + + if self.shouldRefreshing { + self.tableView.refreshControl?.beginRefreshingFromTop() + } + } +} + + +// MARK: SOMSwipableTabBarDelegate + +extension NotificationViewController: SOMSwipableTabBarDelegate { + + func tabBar(_ tabBar: SOMSwipableTabBar, didSelectTabAt index: Int) { + + self.reactor?.action.onNext(.updateDisplayType(index == 1 ? .notice : .activity(.unread))) + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift new file mode 100644 index 00000000..32da85ed --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/NotificationViewReactor.swift @@ -0,0 +1,240 @@ +// +// NotificationViewReactor.swift +// SOOUM +// +// Created by 오현식 on 12/23/24. +// + +import ReactorKit + +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) + } + + enum Mutation { + case notifications(unreads: [CompositeNotificationInfo], reads: [CompositeNotificationInfo]) + case more(unreads: [CompositeNotificationInfo], reads: [CompositeNotificationInfo]) + case notices([NoticeInfo]) + case moreNotices([NoticeInfo]) + case updateDisplayType(DisplayType) + case updateIsRefreshing(Bool) + case updateIsReadSuccess(Bool) + } + + struct State { + fileprivate(set) var displayType: DisplayType + fileprivate(set) var notificationsForUnread: [CompositeNotificationInfo]? + fileprivate(set) var notifications: [CompositeNotificationInfo]? + fileprivate(set) var notices: [NoticeInfo]? + fileprivate(set) var isRefreshing: Bool + fileprivate(set) var isReadSuccess: Bool + } + + var initialState: State + + private let dependencies: AppDIContainerable + private let notificationUseCase: NotificationUseCase + + init(dependencies: AppDIContainerable, displayType: DisplayType = .activity(.unread)) { + self.dependencies = dependencies + self.notificationUseCase = dependencies.rootContainer.resolve(NotificationUseCase.self) + + self.initialState = State( + displayType: displayType, + notificationsForUnread: nil, + notifications: nil, + notices: nil, + isRefreshing: false, + isReadSuccess: false + ) + } + + func mutate(action: Action) -> Observable { + switch action { + case .landing: + + return .concat([ + // Observable.zip( + // self.notificationUseCase.unreadNotifications(lastId: nil), + // self.notificationUseCase.readNotifications(lastId: nil) + // ) + // .map(Mutation.notifications) + // .catch(self.catchClosure), + // self.notificationUseCase.notices(lastId: nil) + // .map(Mutation.notices) + // .catch(self.catchClosure) + .just(.notifications( + unreads: [ + CompositeNotificationInfo.default(.init( + notificationInfo: .init(notificationId: "1", notificationType: .commentWrite, createTime: Date()), + targetCardId: "1", + nickName: "아무개" + )), + CompositeNotificationInfo.default(.init( + notificationInfo: .init(notificationId: "2", notificationType: .feedLike, createTime: Date().addingTimeInterval(-3600)), + targetCardId: "2", + nickName: "아무개" + )), + CompositeNotificationInfo.default(.init( + notificationInfo: .init(notificationId: "3", notificationType: .commentLike, createTime: Date().addingTimeInterval(-3600)), + targetCardId: "3", + nickName: "아무개" + )) + ], + reads: [ + CompositeNotificationInfo.follow(.init( + notificationInfo: .init(notificationId: "4", notificationType: .follow, createTime: Date().addingTimeInterval(-86400)), + nickname: "아무개", + userId: "4" + )), + CompositeNotificationInfo.default(.init( + notificationInfo: .init(notificationId: "5", notificationType: .feedLike, createTime: Date().addingTimeInterval(-2678400)), + targetCardId: "5", + nickName: "아무개" + )) + ] + )), + .just(.notices([ + NoticeInfo(id: "1", noticeType: .news, message: "숨이 새로운 서비스로 찾아올 예정이에요", url: "", createdAt: Date()), + NoticeInfo(id: "2", noticeType: .announcement, message: "숨 공식 인스타그램 안내드려요", url: "", createdAt: Date()), + NoticeInfo(id: "3", noticeType: .maintenance, message: "카드 작성 시 발생했던 오류가 해결됐어요", url: "", createdAt: Date()) + ])) + ]) + case .refresh: + + switch self.currentState.displayType { + case .activity: + return .concat([ + .just(.updateIsRefreshing(true)), + Observable.zip( + self.notificationUseCase.unreadNotifications(lastId: nil), + self.notificationUseCase.readNotifications(lastId: nil) + ) + .map(Mutation.notifications) + .catch(self.catchClosure), + .just(.updateIsRefreshing(false)) + ]) + + case .notice: + return .concat([ + .just(.updateIsRefreshing(true)), + self.notificationUseCase.notices(lastId: nil) + .map(Mutation.notices) + .catch(self.catchClosure), + .just(.updateIsRefreshing(false)) + ]) + } + case let .updateDisplayType(displayType): + return .just(.updateDisplayType(displayType)) + + case let .moreFind(lastId, displayType): + + switch displayType { + case let .activity(activityType): + return .concat([ + self.moreNotification(activityType, with: lastId) + .catch(self.catchClosure) + ]) + case .notice: + return .concat([ + self.notificationUseCase.notices(lastId: lastId) + .map(Mutation.moreNotices) + .catch(self.catchClosure) + ]) + } + case let .requestRead(selectedId): + + return self.notificationUseCase.requestRead(notificationId: selectedId) + .map(Mutation.updateIsReadSuccess) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .notifications(unreads, reads): + newState.notificationsForUnread = unreads + newState.notifications = reads + case let .more(unreads, reads): + newState.notificationsForUnread? += unreads + newState.notifications? += reads + case let .notices(notices): + newState.notices = notices + case let .moreNotices(notices): + newState.notices? += notices + case let .updateDisplayType(displayType): + newState.displayType = displayType + case let .updateIsRefreshing(isRefreshing): + newState.isRefreshing = isRefreshing + case let .updateIsReadSuccess(isReadSuccess): + newState.isReadSuccess = isReadSuccess + } + return newState + } +} + +extension NotificationViewReactor { + + var catchClosure: ((Error) throws -> Observable ) { + return { _ in + .concat([ + .just(.notifications(unreads: [], reads: [])), + .just(.notices([])), + .just(.updateIsRefreshing(false)) + ]) + } + } + + func canUpdateCells( + prev prevStates: DisplayStates, + curr currStates: DisplayStates + ) -> Bool { + return prevStates.displayType == currStates.displayType && + prevStates.unreads == currStates.unreads && + prevStates.reads == currStates.reads && + prevStates.notices == currStates.notices + } +} + +private extension NotificationViewReactor { + + func moreNotification( + _ activityType: DisplayType.ActivityType, + with lastId: String + ) -> Observable { + + switch activityType { + case .unread: + return self.notificationUseCase.unreadNotifications(lastId: lastId) + .map { .more(unreads: $0, reads: []) } + case .read: + return self.notificationUseCase.readNotifications(lastId: lastId) + .map { .more(unreads: [], reads: $0) } + } + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/Views/NotificationViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/Views/NotificationViewController.swift deleted file mode 100644 index b7c8f41e..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/Views/NotificationViewController.swift +++ /dev/null @@ -1,412 +0,0 @@ -// -// NotificationViewController.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import UIKit - -import SnapKit -import Then - -import ReactorKit -import RxCocoa -import RxSwift - - -class NotificationViewController: BaseViewController, View { - - enum Text { - static let withoutReadHeaderTitle: String = "읽지 않음" - } - - enum Section: Int, CaseIterable { - case withoutRead - case read - case empty - } - - - // MARK: Views - - private lazy var tableView = UITableView().then { - $0.backgroundColor = .clear - $0.indicatorStyle = .black - $0.separatorStyle = .none - - $0.isHidden = true - - $0.sectionHeaderTopPadding = .zero - $0.decelerationRate = .fast - - $0.refreshControl = SOMRefreshControl() - - $0.register( - NotificationViewCell.self, - forCellReuseIdentifier: NotificationViewCell.cellIdentifier - ) - $0.register( - NotificationWithReportViewCell.self, - forCellReuseIdentifier: NotificationWithReportViewCell.cellIdentifier - ) - $0.register( - NotiPlaceholderViewCell.self, - forCellReuseIdentifier: NotiPlaceholderViewCell.cellIdentifier - ) - - $0.dataSource = self - $0.delegate = self - } - - - // MARK: Variables - - private var notificationsWithoutRead = [CommentHistoryInNoti]() - private var notifications = [CommentHistoryInNoti]() - private var withoutReadNotisCount = "0" - - private var currentOffset: CGFloat = 0 - private var isRefreshEnabled: Bool = true - private var isLoadingMore: Bool = false - - - // MARK: Variables + Rx - - let willPushCardId = PublishRelay() - - - // MARK: Override func - - override func setupConstraints() { - super.setupConstraints() - - self.view.addSubview(self.tableView) - self.tableView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - } - - - // MARK: ReactorKit - bind - - func bind(reactor: NotificationViewReactor) { - - // Action - self.rx.viewWillAppear - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - self.tableView.refreshControl?.rx.controlEvent(.valueChanged) - .withLatestFrom(reactor.state.map(\.isLoading)) - .filter { $0 == false } - .map { _ in Reactor.Action.refresh } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - // State - reactor.state.map(\.isLoading) - .distinctUntilChanged() - .do(onNext: { [weak self] isLoading in - if isLoading { self?.isLoadingMore = false } - }) - .subscribe(with: self.tableView) { tableView, isLoading in - if isLoading { - tableView.refreshControl?.beginRefreshingFromTop() - } else { - tableView.refreshControl?.endRefreshing() - } - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.isProcessing) - .distinctUntilChanged() - .do(onNext: { [weak self] isProcessing in - if isProcessing { self?.isLoadingMore = false } - }) - .bind(to: self.activityIndicatorView.rx.isAnimating) - .disposed(by: self.disposeBag) - - reactor.state.map(\.withoutReadNotisCount) - .distinctUntilChanged() - .subscribe(with: self) { object, withoutReadNotisCount in - object.withoutReadNotisCount = withoutReadNotisCount - - UIView.performWithoutAnimation { - object.tableView.reloadData() - } - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.notificationsWithoutRead) - .distinctUntilChanged() - .filterNil() - .subscribe(with: self) { object, notificationsWithoutRead in - object.tableView.isHidden = false - - object.notificationsWithoutRead = notificationsWithoutRead - - let indexSetForEmpty = IndexSet(integer: Section.empty.rawValue) - let indexSetForWithoutRead = IndexSet(integer: Section.withoutRead.rawValue) - UIView.performWithoutAnimation { - object.tableView.performBatchUpdates { - object.tableView.reloadSections(indexSetForEmpty, with: .none) - object.tableView.reloadSections(indexSetForWithoutRead, with: .none) - } - } - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.notifications) - .distinctUntilChanged() - .filterNil() - .subscribe(with: self) { object, notifications in - object.tableView.isHidden = false - - object.notifications = notifications - - let indexSetForEmpty = IndexSet(integer: Section.empty.rawValue) - let indexSetForRead = IndexSet(integer: Section.read.rawValue) - UIView.performWithoutAnimation { - object.tableView.performBatchUpdates { - object.tableView.reloadSections(indexSetForEmpty, with: .none) - object.tableView.reloadSections(indexSetForRead, with: .none) - } - } - } - .disposed(by: self.disposeBag) - } -} - -extension NotificationViewController: UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { - return Section.allCases.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - - switch Section.allCases[section] { - case .withoutRead: - return self.notificationsWithoutRead.count - case .read: - return self.notifications.count - case .empty: - return (self.notificationsWithoutRead.isEmpty && self.notifications.isEmpty) ? 1 : 0 - } - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - switch Section.allCases[indexPath.section] { - case .withoutRead: - - let model: CommentHistoryInNoti = self.notificationsWithoutRead[indexPath.row] - switch model.type { - case .blocked, .delete: - - let cell: NotificationWithReportViewCell = tableView.dequeueReusableCell( - withIdentifier: NotificationWithReportViewCell.cellIdentifier, - for: indexPath - ) as! NotificationWithReportViewCell - cell.selectionStyle = .none - cell.bind(model) - - return cell - - default: - - let cell: NotificationViewCell = tableView.dequeueReusableCell( - withIdentifier: NotificationViewCell.cellIdentifier, - for: indexPath - ) as! NotificationViewCell - cell.selectionStyle = .none - cell.bind(model, isReaded: false) - - return cell - } - - case .read: - - let cell: NotificationViewCell = tableView.dequeueReusableCell( - withIdentifier: NotificationViewCell.cellIdentifier, - for: indexPath - ) as! NotificationViewCell - cell.selectionStyle = .none - cell.bind(self.notifications[indexPath.row], isReaded: true) - - return cell - - case .empty: - - let placeholder: NotiPlaceholderViewCell = tableView.dequeueReusableCell( - withIdentifier: NotiPlaceholderViewCell.cellIdentifier, - for: indexPath - ) as! NotiPlaceholderViewCell - - return placeholder - } - } -} - -extension NotificationViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - - switch Section.allCases[indexPath.section] { - case .withoutRead: - let selectedId = self.notificationsWithoutRead[indexPath.row].id - - self.reactor?.action.onNext(.requestRead("\(selectedId)")) - let targetCardId = self.notificationsWithoutRead[indexPath.row].targetCardId - self.willPushCardId.accept("\(targetCardId ?? 0)") - case .read: - let targetCardId = self.notifications[indexPath.row].targetCardId - self.willPushCardId.accept("\(targetCardId ?? 0)") - case .empty: - break - } - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - - switch Section.allCases[section] { - case .withoutRead: - - let backgroundView = UIView().then { - $0.backgroundColor = .som.white - } - - let typography = Typography.som.body2WithBold.withAlignment(.left) - let frame = CGRect(x: 20, y: 16, width: UIScreen.main.bounds.width, height: typography.lineHeight) - let label = UILabel().then { - $0.text = Text.withoutReadHeaderTitle + " (\(self.withoutReadNotisCount)개)" - $0.textColor = .som.black - $0.typography = typography - - $0.frame = frame - } - backgroundView.addSubview(label) - - return self.notificationsWithoutRead.isEmpty ? nil : backgroundView - - case .read: - - let backgroundView = UIView().then { - $0.backgroundColor = .som.white - } - - let frame = CGRect(x: 0, y: 10, width: UIScreen.main.bounds.width, height: 4) - let seperator = UIView().then { - $0.backgroundColor = .som.gray100 - - $0.frame = frame - } - backgroundView.addSubview(seperator) - - return self.notificationsWithoutRead.isEmpty ? nil : backgroundView - - case .empty: - return nil - } - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - - switch Section.allCases[indexPath.section] { - case .withoutRead: - if self.notificationsWithoutRead.isEmpty == false { - let type = self.notificationsWithoutRead[indexPath.row].type - switch type { - case .blocked, .delete: - return 55 - default: - return 64 - } - } else { - return 64 - } - case .read: - return 64 - case .empty: - return self.tableView.bounds.height - } - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - switch Section.allCases[section] { - case .withoutRead: - return self.notificationsWithoutRead.isEmpty ? 0 : 46 - case .read: - return self.notificationsWithoutRead.isEmpty ? 10 : 24 - case .empty: - return 0 - } - } - - func tableView( - _ tableView: UITableView, - willDisplay cell: UITableViewCell, - forRowAt indexPath: IndexPath - ) { - guard self.notificationsWithoutRead.isEmpty == false || - self.notifications.isEmpty == false - else { return } - - let sectionIndexForWithoutRead = Section.withoutRead.rawValue - let lastRowIndexForWithoutRead = tableView.numberOfRows(inSection: sectionIndexForWithoutRead) - 1 - let sectionIndexForRead = Section.read.rawValue - let lastRowIndexForRead = tableView.numberOfRows(inSection: sectionIndexForRead) - 1 - - - if self.isLoadingMore, - indexPath.section == sectionIndexForWithoutRead, - indexPath.row == lastRowIndexForWithoutRead { - - let withoutReadLastId = self.notificationsWithoutRead.last?.id.description - self.reactor?.action.onNext(.moreFind(withoutReadLastId: withoutReadLastId, readLastId: nil)) - } - - if self.isLoadingMore, - indexPath.section == sectionIndexForRead, - indexPath.row == lastRowIndexForRead { - - let readLastId = self.notifications.last?.id.description - self.reactor?.action.onNext(.moreFind(withoutReadLastId: nil, readLastId: readLastId)) - } - } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - - let offset = scrollView.contentOffset.y - - // currentOffset <= 0 && isLoading == false 일 때, 테이블 뷰 새로고침 가능 - self.isRefreshEnabled = (offset <= 0 && self.reactor?.currentState.isLoading == false) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - - let offset = scrollView.contentOffset.y - - // 당겨서 새로고침 상황일 때 - guard offset > 0 else { return } - - // 아래로 스크롤 중일 때, 데이터 추가로드 가능 - self.isLoadingMore = offset > self.currentOffset - self.currentOffset = offset - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - - let offset = scrollView.contentOffset.y - - // isRefreshEnabled == true 이고, 스크롤이 끝났을 경우에만 테이블 뷰 새로고침 - if self.isRefreshEnabled, - let refreshControl = self.tableView.refreshControl, - offset <= -(refreshControl.frame.origin.y + 40) { - - refreshControl.beginRefreshingFromTop() - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/Views/NotificationViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/Views/NotificationViewReactor.swift deleted file mode 100644 index 9d36d54a..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/Views/NotificationViewReactor.swift +++ /dev/null @@ -1,213 +0,0 @@ -// -// NotificationViewReactor.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import ReactorKit - -import Alamofire - - -class NotificationViewReactor: Reactor { - - enum EntranceType { - case total - case comment - case like - } - - enum Action: Equatable { - case landing - case refresh - case moreFind(withoutReadLastId: String?, readLastId: String?) - case requestRead(String) - } - - enum Mutation { - case notificationsWithoutRead([CommentHistoryInNoti]) - case notifications([CommentHistoryInNoti]) - case moreWithoutRead([CommentHistoryInNoti]) - case more([CommentHistoryInNoti]) - case withoutReadNotiscount(String) - case updateIsProcessing(Bool) - case updateIsLoading(Bool) - case updateIsReadCompleted(Bool) - } - - struct State { - var notificationsWithoutRead: [CommentHistoryInNoti]? - var notifications: [CommentHistoryInNoti]? - var withoutReadNotisCount: String - var isProcessing: Bool - var isLoading: Bool - var isReadCompleted: Bool - } - - var initialState: State = .init( - notificationsWithoutRead: nil, - notifications: nil, - withoutReadNotisCount: "0", - isProcessing: false, - isLoading: false, - isReadCompleted: false - ) - - private let entranceType: EntranceType - - let provider: ManagerProviderType - - init(provider: ManagerProviderType, _ entranceType: EntranceType) { - self.provider = provider - self.entranceType = entranceType - } - - func mutate(action: Action) -> Observable { - switch action { - case .landing: - -// let combined = Observable.concat([ -// self.withoutReadNotisCount(), -// self.notifications(with: false), -// self.notifications(with: true) -// ]) -// .delay(.milliseconds(500), scheduler: MainScheduler.instance) -// -// return .concat([ -// .just(.updateIsProcessing(true)), -// combined, -// .just(.updateIsProcessing(false)) -// ]) - return .empty() - case .refresh: - -// let combined = Observable.concat([ -// self.withoutReadNotisCount(), -// self.notifications(with: false), -// self.notifications(with: true) -// ]) -// .delay(.milliseconds(500), scheduler: MainScheduler.instance) -// -// return .concat([ -// .just(.updateIsLoading(true)), -// combined, -// .just(.updateIsLoading(false)) -// ]) - return .empty() - case let .moreFind(withoutReadLastId, readLastId): - -// let combined = Observable.concat([ -// self.withoutReadNotisCount(), -// self.moreNotifications(with: false, lastId: withoutReadLastId), -// self.moreNotifications(with: true, lastId: readLastId) -// ]) -// .delay(.milliseconds(500), scheduler: MainScheduler.instance) -// -// return .concat([ -// .just(.updateIsProcessing(true)), -// combined, -// .just(.updateIsProcessing(false)) -// ]) - return .empty() - - case let .requestRead(selectedId): - self.provider.pushManager.deleteNotification(notificationId: selectedId) - let request: NotificationRequest = .requestRead(notificationId: selectedId) - return self.provider.networkManager.request(Empty.self, request: request) - .map { _ in .updateIsReadCompleted(true) } - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var state = state - switch mutation { - case let .notificationsWithoutRead(notificationsWithoutRead): - state.notificationsWithoutRead = notificationsWithoutRead - case let .notifications(notifications): - state.notifications = notifications - case let .moreWithoutRead(notificationsWithoutRead): - state.notificationsWithoutRead? += notificationsWithoutRead - case let .more(notifications): - state.notifications? += notifications - case let .withoutReadNotiscount(withoutReadNotisCount): - state.withoutReadNotisCount = withoutReadNotisCount - case let .updateIsProcessing(isProcessing): - state.isProcessing = isProcessing - case let .updateIsLoading(isLoading): - state.isLoading = isLoading - case let .updateIsReadCompleted(isReadCompleted): - state.isReadCompleted = isReadCompleted - } - return state - } -} - -//extension NotificationViewReactor { -// -// private func notifications(with isRead: Bool) -> Observable { -// -// var request: NotificationRequest { -// switch self.entranceType { -// case .total: -// return isRead ? .totalRead(lastId: nil) : .totalWithoutRead(lastId: nil) -// case .comment: -// return isRead ? .commentRead(lastId: nil) : .commentWithoutRead(lastId: nil) -// case .like: -// return isRead ? .likeRead(lastId: nil) : .likeWithoutRead(lastId: nil) -// } -// } -// -// return self.provider.networkManager.request(CommentHistoryInNotiResponse.self, request: request) -// .map(\.commentHistoryInNotis) -// .map(isRead ? Mutation.notifications : Mutation.notificationsWithoutRead) -// .catch(self.catchClosure) -// } -// -// private func moreNotifications(with isRead: Bool, lastId: String?) -> Observable { -// -// guard let lastId = lastId else { return .just(.more([])) } -// -// var request: NotificationRequest { -// switch self.entranceType { -// case .total: -// return isRead ? .totalRead(lastId: lastId) : .totalWithoutRead(lastId: lastId) -// case .comment: -// return isRead ? .commentRead(lastId: lastId) : .commentWithoutRead(lastId: lastId) -// case .like: -// return isRead ? .likeRead(lastId: lastId) : .likeWithoutRead(lastId: lastId) -// } -// } -// -// return self.provider.networkManager.request(CommentHistoryInNotiResponse.self, request: request) -// .map(\.commentHistoryInNotis) -// .map(isRead ? Mutation.more : Mutation.moreWithoutRead) -// .catch(self.catchClosure) -// } -// -// private func withoutReadNotisCount() -> Observable { -// -// var request: NotificationRequest { -// switch self.entranceType { -// case .total: -// return .totalWithoutReadCount -// case .comment: -// return .commentWithoutReadCount -// case .like: -// return .likeWihoutReadCount -// } -// } -// -// return self.provider.networkManager.request(WithoutReadNotisCountResponse.self, request: request) -// .map(\.unreadCnt) -// .map(Mutation.withoutReadNotiscount) -// } -// -// var catchClosure: ((Error) throws -> Observable ) { -// return { _ in -// .concat([ -// .just(.updateIsProcessing(false)) -// ]) -// } -// } -//} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NoticeViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NoticeViewCell.swift new file mode 100644 index 00000000..bf50aedf --- /dev/null +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NoticeViewCell.swift @@ -0,0 +1,111 @@ +// +// NoticeViewCell.swift +// SOOUM +// +// Created by 오현식 on 9/26/25. +// + +import UIKit + +import SnapKit +import Then + + +class NoticeViewCell: UITableViewCell { + + enum Text { + static let title: String = "공지사항" + } + + static let cellIdentifier = String(reflecting: NoticeViewCell.self) + + + // MARK: Views + + private let iconView = UIImageView().then { + $0.image = .init(.icon(.v2(.filled(.notice)))) + $0.tintColor = .som.v2.rMain + } + + private let titleLabel = UILabel().then { + $0.text = Text.title + $0.textColor = .som.v2.gray400 + $0.typography = .som.v2.caption2 + } + + private let timeLabel = UILabel().then { + $0.textColor = .som.gray400 + $0.typography = .som.v2.caption2 + } + + private let contentLabel = UILabel().then { + $0.textColor = .som.v2.gray600 + $0.typography = .som.v2.subtitle1.withAlignment(.left) + $0.numberOfLines = 0 + $0.textAlignment = .left + } + + + // MARK: Override func + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.selectionStyle = .none + + self.setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Private func + + private func setupConstraints() { + + let titleContinaer = UIView() + self.contentView.addSubview(titleContinaer) + titleContinaer.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.leading.equalToSuperview().offset(24) + $0.trailing.equalToSuperview().offset(-24) + } + + titleContinaer.addSubview(self.iconView) + self.iconView.snp.makeConstraints { + $0.centerY.leading.equalToSuperview() + $0.size.equalTo(16) + } + + titleContinaer.addSubview(self.titleLabel) + self.titleLabel.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.leading.equalTo(self.iconView.snp.trailing).offset(8) + } + + titleContinaer.addSubview(self.timeLabel) + self.timeLabel.snp.makeConstraints { + $0.verticalEdges.trailing.equalToSuperview() + $0.leading.greaterThanOrEqualTo(self.titleLabel.snp.trailing).offset(8) + } + + self.contentView.addSubview(self.contentLabel) + self.contentLabel.snp.makeConstraints { + $0.top.equalTo(titleContinaer.snp.bottom).offset(4) + $0.bottom.equalToSuperview().offset(-16) + $0.leading.equalToSuperview().offset(48) + $0.trailing.equalToSuperview().offset(-24) + } + } + + func bind(_ model: NoticeInfo) { + + let timeAttributes = Typography.som.v2.caption2.attributes + self.timeLabel.attributedText = .init(string: model.createdAt.noticeFormatted, attributes: timeAttributes) + + let contentsAttributes = Typography.som.v2.subtitle1.withAlignment(.left).attributes + self.contentLabel.attributedText = .init(string: model.message, attributes: contentsAttributes) + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotiPlaceholderViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift similarity index 56% rename from SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotiPlaceholderViewCell.swift rename to SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift index b56650b0..16562323 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotiPlaceholderViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationPlaceholderViewCell.swift @@ -1,5 +1,5 @@ // -// NotiPlaceholderViewCell.swift +// NotificationPlaceholderViewCell.swift // SOOUM // // Created by 오현식 on 1/9/25. @@ -11,21 +11,25 @@ import SnapKit import Then -class NotiPlaceholderViewCell: UITableViewCell { - - static let cellIdentifier = String(reflecting: NotiPlaceholderViewCell.self) +class NotificationPlaceholderViewCell: UITableViewCell { enum Text { - static let placeholderLabelText: String = "알림이 아직 없어요" + static let placeholderLabelText: String = "아직 표시할 알림이 없어요" } + static let cellIdentifier = String(reflecting: NotificationPlaceholderViewCell.self) + // MARK: Views + private let placeholderImage = UIImageView().then { + $0.image = .init(.image(.v2(.placeholder_notification))) + } + private let placeholderLabel = UILabel().then { $0.text = Text.placeholderLabelText - $0.textColor = .init(hex: "#B4B4B4") - $0.typography = .som.body1WithBold + $0.textColor = .som.v2.gray400 + $0.typography = .som.v2.body1 } @@ -35,7 +39,6 @@ class NotiPlaceholderViewCell: UITableViewCell { super.init(style: style, reuseIdentifier: reuseIdentifier) self.selectionStyle = .none - self.backgroundColor = .clear self.isUserInteractionEnabled = false self.setupConstraints() @@ -50,9 +53,17 @@ class NotiPlaceholderViewCell: UITableViewCell { private func setupConstraints() { + self.contentView.addSubview(self.placeholderImage) + self.placeholderImage.snp.makeConstraints { + $0.top.equalToSuperview().offset(UIScreen.main.bounds.height * 0.2) + $0.centerX.equalToSuperview() + } + self.contentView.addSubview(self.placeholderLabel) + self.placeholderLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(UIScreen.main.bounds.height * 0.3) + $0.top.equalTo(self.placeholderImage.snp.bottom).offset(20) + $0.bottom.equalToSuperview() $0.centerX.equalToSuperview() } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift index ca8e1b52..f5a3fd84 100644 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationViewCell.swift @@ -13,50 +13,50 @@ import Then class NotificationViewCell: UITableViewCell { + enum Text { + static let cardTitle: String = "카드" + static let feedLikeContents: String = "님이 회원님의 카드에 좋아요를 남겼어요." + static let commentLikeContents: String = "님이 회원님의 답카드에 좋아요를 남겼어요." + static let commentWriteContents: String = "님이 답카드를 남겼어요. 알림을 눌러 대화를 이어가 보세요." + + static let followTitle: String = "팔로우" + static let followContents: String = "님이 회원님을 팔로우하기 시작했어요." + + static let deletedAndBlockedTitle: String = "제한" + static let deletedContents: String = "운영정책 위반으로 인해 작성된 카드가 삭제 처리되었습니다." + static let blockedLeadingContents: String = "운영정책 위반으로 인해 " + static let blockedTrailingContents: String = "까지 카드추가가 제한됩니다." + } + static let cellIdentifier = String(reflecting: NotificationViewCell.self) - private let feedCardImageView = UIImageView().then { - $0.layer.cornerRadius = 6 - $0.clipsToBounds = true - } - private let feedCardDimView = UIView().then { - $0.backgroundColor = .black.withAlphaComponent(0.3) - } + // MARK: Views - private let feedCardContentLabel = UILabel().then { - $0.textColor = .som.white - $0.textAlignment = .center - $0.numberOfLines = 0 - $0.lineBreakMode = .byTruncatingTail - $0.typography = .init( - fontContainer: BuiltInFont(size: 3, weight: .bold), - lineHeight: 5, - letterSpacing: -0.04 - ) - } + private let iconView = UIImageView() - private let notificationTitleLabel = UILabel().then { - $0.textColor = .som.gray700 - $0.textAlignment = .center - $0.typography = .som.body3WithBold + private let titleLabel = UILabel().then { + $0.textColor = .som.v2.gray400 } private let timeGapLabel = UILabel().then { $0.textColor = .som.gray400 - $0.textAlignment = .center - $0.typography = .som.body3WithBold } - private let dotWithoutReadView = UIView().then { - $0.backgroundColor = .som.red - $0.layer.cornerRadius = 6 * 0.5 - $0.clipsToBounds = true + private let contentLabel = UILabel().then { + $0.textColor = .som.v2.gray600 + $0.numberOfLines = 0 + $0.textAlignment = .left } + + // MARK: Override func + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + self.selectionStyle = .none + self.setupConstraints() } @@ -64,63 +64,135 @@ class NotificationViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } + + // MARK: Private func + private func setupConstraints() { - self.contentView.addSubview(self.feedCardImageView) - self.feedCardImageView.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalToSuperview().offset(20) - $0.size.equalTo(40) - } - - self.feedCardImageView.addSubview(self.feedCardDimView) - self.feedCardDimView.snp.makeConstraints { - $0.edges.equalToSuperview() + let titleContinaer = UIView() + self.contentView.addSubview(titleContinaer) + titleContinaer.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.leading.equalToSuperview().offset(24) + $0.trailing.equalToSuperview().offset(-24) } - self.feedCardImageView.addSubview(self.feedCardContentLabel) - self.feedCardContentLabel.snp.makeConstraints { - $0.top.leading.equalToSuperview().offset(5) - $0.bottom.trailing.equalToSuperview().offset(-5) + titleContinaer.addSubview(self.iconView) + self.iconView.snp.makeConstraints { + $0.centerY.leading.equalToSuperview() + $0.size.equalTo(16) } - self.contentView.addSubview(self.notificationTitleLabel) - self.notificationTitleLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalTo(self.feedCardImageView.snp.trailing).offset(20) + titleContinaer.addSubview(self.titleLabel) + self.titleLabel.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.leading.equalTo(self.iconView.snp.trailing).offset(8) } - self.contentView.addSubview(self.timeGapLabel) + titleContinaer.addSubview(self.timeGapLabel) self.timeGapLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.greaterThanOrEqualTo(self.notificationTitleLabel.snp.trailing).offset(9) - $0.trailing.equalToSuperview().offset(-26) + $0.verticalEdges.trailing.equalToSuperview() + $0.leading.greaterThanOrEqualTo(self.titleLabel.snp.trailing).offset(8) } - self.contentView.addSubview(self.dotWithoutReadView) - self.dotWithoutReadView.snp.makeConstraints { - $0.top.equalTo(self.timeGapLabel.snp.top) - $0.leading.equalTo(self.timeGapLabel.snp.trailing) - $0.size.equalTo(6) + self.contentView.addSubview(self.contentLabel) + self.contentLabel.snp.makeConstraints { + $0.top.equalTo(titleContinaer.snp.bottom).offset(4) + $0.bottom.equalToSuperview().offset(-16) + $0.leading.equalToSuperview().offset(48) + $0.trailing.equalToSuperview().offset(-24) } } - func bind(_ model: CommentHistoryInNoti, isReaded: Bool) { + func bind(_ model: CompositeNotificationInfo, isReaded: Bool) { - self.feedCardImageView.setImage(strUrl: model.feedCardImgURL?.url) - self.feedCardContentLabel.text = model.content + self.backgroundColor = isReaded ? .som.v2.white : .som.v2.pLight1 - let text: String = { - switch model.type { - case .feedLike, .commentLike: return "님이 카드에 공감하였습니다." - case .commentWrite: return "님이 답카드를 작성했습니다." - default: return "" + var iconInfo: (image: UIImage?, color: UIColor)? { + switch model { + case .default: + return (.init(.icon(.v2(.filled(.card)))), .som.v2.pMain) + case .follow: + return (.init(.icon(.v2(.filled(.users)))), .som.v2.pMain) + case .deleted, .blocked: + return (.init(.icon(.v2(.filled(.danger)))), .som.v2.yMain) } - }() - self.notificationTitleLabel.text = "\(model.nickName ?? "")\(text)" + } + + var titleInfo: (text: String, typography: Typography)? { + let typography = isReaded ? Typography.som.v2.caption2 : Typography.som.v2.caption1 + switch model { + case .default: + return (Text.cardTitle, typography) + case .follow: + return (Text.followTitle, typography) + case .deleted: + return (Text.deletedAndBlockedTitle, typography) + case .blocked: + return (Text.deletedAndBlockedTitle, typography) + } + } + + var timeGapInfo: (text: String, typography: Typography)? { + let typography = isReaded ? Typography.som.v2.caption2 : Typography.som.v2.caption1 + switch model { + case let .default(notification): + let timeGapText = notification.notificationInfo.createTime.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) + return (timeGapText, typography) + case let .follow(notification): + let timeGapText = notification.notificationInfo.createTime.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) + return (timeGapText, typography) + case let .deleted(notification): + let timeGapText = notification.notificationInfo.createTime.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) + return (timeGapText, typography) + case let .blocked(notification): + let timeGapText = notification.notificationInfo.createTime.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) + return (timeGapText, typography) + } + } + + var contentsInfo: (text: String, typography: Typography)? { + let typography = isReaded ? Typography.som.v2.subtitle1.withAlignment(.left) : Typography.som.v2.title2.withAlignment(.left) + switch model { + case let .default(notification): + switch notification.notificationInfo.notificationType { + case .feedLike: + return ("\(notification.nickName)\(Text.feedLikeContents)", typography) + case .commentLike: + return ("\(notification.nickName)\(Text.commentLikeContents)", typography) + case .commentWrite: + return ("\(notification.nickName)\(Text.commentWriteContents)", typography) + default: + return nil + } + case let .follow(notification): + return ("\(notification.nickname)\(Text.followContents)", typography) + case .deleted: + return (Text.deletedContents, typography) + case let .blocked(notification): + let text = "\(Text.blockedLeadingContents)\(notification.blockExpirationDateTime.banEndFormatted)\(Text.blockedTrailingContents)" + return (text, typography) + } + } + + if let iconInfo = iconInfo { + self.iconView.image = iconInfo.image + self.iconView.tintColor = iconInfo.color + } - self.timeGapLabel.text = model.createAt.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) + if let titleInfo = titleInfo { + self.titleLabel.attributedText = .init(string: titleInfo.text, attributes: titleInfo.typography.attributes) + self.titleLabel.typography = titleInfo.typography + } + + if let timeGapInfo = timeGapInfo { + self.timeGapLabel.attributedText = .init(string: timeGapInfo.text, attributes: timeGapInfo.typography.attributes) + self.timeGapLabel.typography = timeGapInfo.typography + } - self.dotWithoutReadView.isHidden = isReaded + if let contentsInfo = contentsInfo { + self.contentLabel.attributedText = .init(string: contentsInfo.text, attributes: contentsInfo.typography.attributes) + self.contentLabel.typography = contentsInfo.typography + } } } diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationWithReportViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationWithReportViewCell.swift deleted file mode 100644 index 96fdafa0..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Notification/cells/NotificationWithReportViewCell.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// NotificationWithReportViewCell.swift -// SOOUM -// -// Created by 오현식 on 12/20/24. -// - -import UIKit - -import SnapKit -import Then - - -class NotificationWithReportViewCell: UITableViewCell { - - static let cellIdentifier = String(reflecting: NotificationWithReportViewCell.self) - - enum Text { - static let blockTypeText: String = "[정지]" - static let deleteTypeText: String = "[삭제]" - - static let blockTitleText: String = "까지 글쓰기가 제한되었습니다." - static let deleteTitleText: String = "신고로 인해 카드가 삭제 처리 되었습니다." - } - - private let typeLabel = UILabel().then { - $0.textColor = .som.red - $0.textAlignment = .center - $0.typography = .init( - fontContainer: BuiltInFont(size: 12, weight: .medium), - lineHeight: 17, - letterSpacing: -0.004 - ) - } - - private let titleLabel = UILabel().then { - $0.textColor = .som.gray700 - $0.textAlignment = .center - $0.typography = .som.body3WithBold - } - - private let timeGapLabel = UILabel().then { - $0.textColor = .som.gray400 - $0.textAlignment = .center - $0.typography = .som.body3WithRegular - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - self.setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - - self.contentView.addSubview(self.typeLabel) - self.typeLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalToSuperview().offset(20) - } - - self.contentView.addSubview(self.titleLabel) - self.titleLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalTo(self.typeLabel.snp.trailing).offset(2) - } - - self.contentView.addSubview(self.timeGapLabel) - self.timeGapLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.greaterThanOrEqualTo(self.titleLabel.snp.trailing) - $0.trailing.equalToSuperview().offset(-26) - } - } - - func bind(_ model: CommentHistoryInNoti) { - - var typeText: String { - switch model.type { - case .blocked: return Text.blockTypeText - case .delete: return Text.deleteTypeText - default: return "" - } - } - self.typeLabel.text = typeText - - var titleText: String { - switch model.type { - case .blocked: return (model.blockExpirationTime ?? Date()).banEndFormatted + Text.blockTitleText - case .delete: return Text.deleteTitleText - default: return "" - } - } - self.titleLabel.text = titleText - - self.timeGapLabel.text = model.createAt.toKorea().infoReadableTimeTakenFromThis(to: Date().toKorea()) - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Popular/MainHomePopularViewController.swift b/SOOUM/SOOUM/Presentations/Main/Home/Popular/MainHomePopularViewController.swift deleted file mode 100644 index 0fbbe18e..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Popular/MainHomePopularViewController.swift +++ /dev/null @@ -1,273 +0,0 @@ -// -// MainHomePopularViewController.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import UIKit - -import Kingfisher -import SnapKit -import Then - -import ReactorKit -import RxCocoa -import RxSwift - - -class MainHomePopularViewController: BaseViewController, View { - - - // MARK: Views - - private lazy var tableView = UITableView(frame: .zero, style: .plain).then { - $0.backgroundColor = .clear - $0.indicatorStyle = .black - $0.separatorStyle = .none - - $0.contentInset.top = SOMSwipeTabBar.Height.mainHome - - $0.isHidden = true - - $0.register(MainHomeViewCell.self, forCellReuseIdentifier: "cell") - $0.register(PlaceholderViewCell.self, forCellReuseIdentifier: "placeholder") - - $0.refreshControl = SOMRefreshControl() - - $0.dataSource = self - $0.prefetchDataSource = self - - $0.delegate = self - } - - private let moveTopButton = MoveTopButtonView().then { - $0.isHidden = true - } - - - // MARK: Variables - - // tableView 정보 - private var currentOffset: CGFloat = 0 - private var isRefreshEnabled: Bool = true - - private let cellHeight: CGFloat = { - let width: CGFloat = (UIScreen.main.bounds.width - 20 * 2) * 0.9 - return width + 10 /// 가로 + top inset - }() - - - // MARK: Variables + Rx - - let hidesHeaderContainer = PublishRelay() - let willPushCardId = PublishRelay() - - - // MARK: Override func - - override func setupConstraints() { - super.setupConstraints() - - self.view.addSubview(self.tableView) - self.tableView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - self.view.addSubview(self.moveTopButton) - self.view.bringSubviewToFront(self.moveTopButton) - self.moveTopButton.snp.makeConstraints { - let bottomOffset: CGFloat = 24 + 60 + 4 + 20 - $0.bottom.equalTo(self.tableView.snp.bottom).offset(-bottomOffset) - $0.centerX.equalToSuperview() - $0.height.equalTo(MoveTopButtonView.height) - } - } - - override func bind() { - super.bind() - - // tableView 상단 이동 - self.moveTopButton.backgroundButton.rx.throttleTap(.seconds(3)) - .subscribe(with: self) { object, _ in - let indexPath = IndexPath(row: 0, section: 0) - object.tableView.scrollToRow(at: indexPath, at: .top, animated: true) - } - .disposed(by: self.disposeBag) - } - - - // MARK: ReactorKit - bind - - func bind(reactor: MainHomePopularViewReactor) { - - // Action - self.rx.viewWillAppear - .map { _ in Reactor.Action.landing } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - let isLoading = reactor.state.map(\.isLoading).distinctUntilChanged().share() - self.tableView.refreshControl?.rx.controlEvent(.valueChanged) - .withLatestFrom(isLoading) - .filter { $0 == false } - .map { _ in Reactor.Action.refresh } - .bind(to: reactor.action) - .disposed(by: self.disposeBag) - - // State - isLoading - .subscribe(with: self.tableView) { tableView, isLoading in - if isLoading { - tableView.refreshControl?.beginRefreshingFromTop() - } else { - tableView.refreshControl?.endRefreshing() - } - } - .disposed(by: self.disposeBag) - - reactor.state.map(\.isProcessing) - .distinctUntilChanged() - .bind(to: self.activityIndicatorView.rx.isAnimating) - .disposed(by: self.disposeBag) - - reactor.state.map(\.displayedCards) - .filterNil() - .distinctUntilChanged() - .subscribe(with: self) { object, displayedCards in - object.tableView.isHidden = false - - object.tableView.reloadData() - } - .disposed(by: self.disposeBag) - } -} - -extension MainHomePopularViewController { - - private func cellForPlaceholder(_ tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { - - let placeholder = tableView.dequeueReusableCell( - withIdentifier: "placeholder", - for: indexPath - ) as! PlaceholderViewCell - - return placeholder - } - - private func cellForMainHome( - _ tableView: UITableView, - for indexPath: IndexPath, - with reactor: MainHomePopularViewReactor - ) -> UITableViewCell { - guard let displayedCards = reactor.currentState.displayedCards else { return .init(frame: .zero) } - - let model = SOMCardModel(data: displayedCards[indexPath.row]) - let cell: MainHomeViewCell = tableView.dequeueReusableCell( - withIdentifier: "cell", - for: indexPath - ) as! MainHomeViewCell - cell.setModel(model) - // 카드 하단 contents 스택 순서 변경 (인기순) - cell.changeOrderInCardContentStack(1) - - return cell - } -} - - -// MARK: MainHomeViewController DataSource and Delegate - -extension MainHomePopularViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.reactor?.currentState.displayedCardsCount ?? 1 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let reactor = self.reactor else { return .init(frame: .zero) } - - if reactor.currentState.isDisplayedCardsEmpty { - - return self.cellForPlaceholder(tableView, for: indexPath) - } else { - - return self.cellForMainHome(tableView, for: indexPath, with: reactor) - } - } -} - -extension MainHomePopularViewController: UITableViewDataSourcePrefetching { - - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - guard let reactor = self.reactor, - let displayedCards = reactor.currentState.displayedCards - else { return } - - indexPaths.forEach { indexPath in - // 데이터 로드 전, 이미지 캐싱 - let strUrl = displayedCards[indexPath.row].backgroundImgURL.url - KingfisherManager.shared.download(strUrl: strUrl) { _ in } - } - } -} - -extension MainHomePopularViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let reactor = self.reactor, - let selectedId = reactor.currentState.displayedCards?[indexPath.row].id - else { return } - - self.willPushCardId.accept(selectedId) - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return (self.reactor?.currentState.isDisplayedCardsEmpty ?? true) ? tableView.bounds.height : self.cellHeight - } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - - // currentOffset <= 0 && isLoading == false 일 때, 테이블 뷰 새로고침 가능 - self.isRefreshEnabled = (self.currentOffset <= 0 && self.reactor?.currentState.isLoading == false) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - - let offset = scrollView.contentOffset.y - - // 당겨서 새로고침 상황일 때 - if offset <= 0 { - - self.hidesHeaderContainer.accept(false) - self.currentOffset = offset - self.moveTopButton.isHidden = true - - return - } - - guard offset <= (scrollView.contentSize.height - scrollView.frame.height) else { return } - - // offset이 currentOffset보다 크면 아래로 스크롤, 반대일 경우 위로 스크롤 - // 위로 스크롤 중일 때 헤더뷰 표시, 아래로 스크롤 중일 때 헤더뷰 숨김 - self.hidesHeaderContainer.accept(offset > self.currentOffset) - - self.currentOffset = offset - - // 최상단일 때만 moveToButton 숨김 - self.moveTopButton.isHidden = self.currentOffset <= 0 - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - - let offset = scrollView.contentOffset.y - - // isRefreshEnabled == true 이고, 스크롤이 끝났을 경우에만 테이블 뷰 새로고침 - if self.isRefreshEnabled, - let refreshControl = self.tableView.refreshControl, - offset <= -(refreshControl.frame.origin.y + 40) { - - refreshControl.beginRefreshingFromTop() - } - } -} diff --git a/SOOUM/SOOUM/Presentations/Main/Home/Popular/MainHomePopularViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Home/Popular/MainHomePopularViewReactor.swift deleted file mode 100644 index c8e10f45..00000000 --- a/SOOUM/SOOUM/Presentations/Main/Home/Popular/MainHomePopularViewReactor.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// MainHomePopularViewReactor.swift -// SOOUM -// -// Created by 오현식 on 12/23/24. -// - -import ReactorKit - - -class MainHomePopularViewReactor: Reactor { - - enum Action: Equatable { - case landing - case refresh - } - - enum Mutation { - case cards([Card]) - case updateIsLoading(Bool) - case updateIsProcessing(Bool) - } - - struct State { - fileprivate(set) var displayedCards: [Card]? - fileprivate(set) var isLoading: Bool - fileprivate(set) var isProcessing: Bool - - var isDisplayedCardsEmpty: Bool { - return self.displayedCards?.isEmpty ?? true - } - var displayedCardsCount: Int { - return self.isDisplayedCardsEmpty ? 1 : (self.displayedCards?.count ?? 1) - } - } - - var initialState: State = .init( - displayedCards: nil, - isLoading: false, - isProcessing: false - ) - - let provider: ManagerProviderType - - // TODO: 페이징 - // private let countPerLoading: Int = 10 - - init(provider: ManagerProviderType) { - self.provider = provider - } - - - func mutate(action: Action) -> Observable { - switch action { - case .landing: - - return .concat([ - .just(.updateIsProcessing(true)), - self.refresh() - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsProcessing(false)) - ]) - case .refresh: - return .concat([ - .just(.updateIsLoading(true)), - self.refresh() - .delay(.milliseconds(500), scheduler: MainScheduler.instance), - .just(.updateIsLoading(false)) - ]) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var state: State = state - switch mutation { - case let .cards(displayedCards): - state.displayedCards = displayedCards - case let .updateIsLoading(isLoading): - state.isLoading = isLoading - case let .updateIsProcessing(isProcessing): - state.isProcessing = isProcessing - } - return state - } -} - -extension MainHomePopularViewReactor { - - func refresh() -> Observable { - - let latitude = self.provider.locationManager.coordinate.latitude - let longitude = self.provider.locationManager.coordinate.longitude - - let request: CardRequest = .popularCard(latitude: latitude, longitude: longitude) - return self.provider.networkManager.request(PopularCardResponse.self, request: request) - .map(\.embedded.cards) - .map(Mutation.cards) - .catch(self.catchClosure) - } -} - -extension MainHomePopularViewReactor { - - var catchClosure: ((Error) throws -> Observable ) { - return { _ in - .concat([ - .just(.cards([])), - .just(.updateIsProcessing(false)), - .just(.updateIsLoading(false)) - ]) - } - } - - // TODO: 페이징 - // func separate(displayed displayedCards: [Card], current cards: [Card]) -> [Card] { - // let count = displayedCards.count - // let displayedCards = Array(cards[count.. Bool { - if viewController.tabBarItem.tag == 1 { + // if viewController.tabBarItem.tag == 1 { + // + // let writeCardViewController = WriteCardViewController() + // writeCardViewController.reactor = self.reactor?.reactorForWriteCard() + // if let selectedViewController = tabBarController.selectedViewController { + // selectedViewController.navigationPush(writeCardViewController, animated: true) + // } + // return false + // } - let writeCardViewController = WriteCardViewController() - writeCardViewController.reactor = self.reactor?.reactorForWriteCard() - if let selectedViewController = tabBarController.selectedViewController { - selectedViewController.navigationPush(writeCardViewController, animated: true) - } + if viewController.tabBarItem.tag == 1 || viewController.tabBarItem.tag == 2 || viewController.tabBarItem.tag == 3 { + self.showPrepare() + return false } - + return true } - func tabBarController( _ tabBarController: SOMTabBarController, didSelect viewController: UIViewController ) { } } + + +// MARK: Prepare dialog + +extension MainTabBarController { + + func showPrepare() { + + let confirmAction = SOMDialogAction( + title: "확인", + style: .primary, + action: { + UIApplication.topViewController?.dismiss(animated: true) + } + ) + + SOMDialogViewController.show( + title: "서비스 준비중입니다.", + message: "추후 개발 완료되면 사용가능합니다.", + actions: [confirmAction] + ) + } +} diff --git a/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift b/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift index 0e36f1ab..89a3c638 100644 --- a/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/MainTabBarReactor.swift @@ -41,10 +41,14 @@ class MainTabBarReactor: Reactor { private let willNavigate: EntranceType let pushInfo: NotificationInfo? - let provider: ManagerProviderType - - init(provider: ManagerProviderType, pushInfo: NotificationInfo? = nil) { - self.provider = provider + private let dependencies: AppDIContainerable + let pushManager: PushManagerDelegate + let locationManager: LocationManagerDelegate + + init(dependencies: AppDIContainerable, pushInfo: NotificationInfo? = nil) { + self.dependencies = dependencies + self.pushManager = dependencies.rootContainer.resolve(ManagerProviderType.self).pushManager + self.locationManager = dependencies.rootContainer.resolve(ManagerProviderType.self).locationManager var willNavigate: EntranceType { switch pushInfo?.notificationType { @@ -61,7 +65,7 @@ class MainTabBarReactor: Reactor { self.initialState = .init( entranceType: .none, - notificationStatus: provider.pushManager.notificationStatus + notificationStatus: self.pushManager.notificationStatus ) } @@ -70,7 +74,7 @@ class MainTabBarReactor: Reactor { case .judgeEntrance: return .concat([ .just(.updateEntrance), - self.provider.pushManager.switchNotification(on: true) + self.pushManager.switchNotification(on: true) .flatMapLatest { error -> Observable in .empty() } ]) case let .updateNotificationStatus(status): @@ -92,38 +96,27 @@ class MainTabBarReactor: Reactor { extension MainTabBarReactor { - private func subscribe() { - - (self.provider.pushManager as? PushManager)?.rx.observe(\.notificationStatus) - .map(Action.updateNotificationStatus) - .bind(to: self.action) - .disposed(by: self.disposeBag) + func reactorForHome() -> HomeViewReactor { + HomeViewReactor(dependencies: self.dependencies) } -} - -extension MainTabBarReactor { - func reactorForMainHome() -> MainHomeTabBarReactor { - MainHomeTabBarReactor(provider: self.provider) - } + // func reactorForWriteCard() -> WriteCardViewReactor { + // WriteCardViewReactor(provider: self.provider, type: .card) + // } - func reactorForWriteCard() -> WriteCardViewReactor { - WriteCardViewReactor(provider: self.provider, type: .card) - } + // func reactorForTags() -> TagsViewReactor { + // TagsViewReactor(provider: self.provider) + // } - func reactorForTags() -> TagsViewReactor { - TagsViewReactor(provider: self.provider) - } + // func reactorForProfile() -> ProfileViewReactor { + // ProfileViewReactor(provider: self.provider, type: .my, memberId: nil) + // } - func reactorForProfile() -> ProfileViewReactor { - ProfileViewReactor(provider: self.provider, type: .my, memberId: nil) + func reactorForNoti() -> NotificationViewReactor { + NotificationViewReactor(dependencies: self.dependencies) } - func reactorForNoti() -> NotificationTabBarReactor { - NotificationTabBarReactor(provider: self.provider) - } - - func reactorForDetail(_ targetCardId: String) -> DetailViewReactor { - DetailViewReactor(provider: self.provider, type: .push, targetCardId) - } + // func reactorForDetail(_ targetCardId: String) -> DetailViewReactor { + // DetailViewReactor(provider: self.provider, type: .push, targetCardId) + // } } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/MyProfileViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/MyProfileViewCell.swift index a5d4a688..627e3541 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/MyProfileViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/MyProfileViewCell.swift @@ -178,7 +178,7 @@ class MyProfileViewCell: UICollectionViewCell { func setModel(_ profile: Profile) { if let profileImg = profile.profileImg { - self.profileImageView.setImage(strUrl: profileImg.url) + self.profileImageView.setImage(strUrl: profileImg.url, with: "") } else { self.profileImageView.image = .init(.image(.defaultStyle(.sooumLogo))) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/OtherProfileViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/OtherProfileViewCell.swift index 6db75487..38620704 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/OtherProfileViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/OtherProfileViewCell.swift @@ -183,7 +183,7 @@ class OtherProfileViewCell: UICollectionViewCell { func setModel(_ profile: Profile, isBlocked: Bool) { if let profileImg = profile.profileImg { - self.profileImageView.setImage(strUrl: profileImg.url) + self.profileImageView.setImage(strUrl: profileImg.url, with: "") } else { self.profileImageView.image = .init(.image(.defaultStyle(.sooumLogo))) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileViewFooterCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileViewFooterCell.swift index f9b9b214..f8219d60 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileViewFooterCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Cells/ProfileViewFooterCell.swift @@ -66,7 +66,7 @@ class ProfileViewFooterCell: UICollectionViewCell { } func setModel(_ writtenCard: WrittenCard) { - self.backgroundImageView.setImage(strUrl: writtenCard.backgroundImgURL.url) + self.backgroundImageView.setImage(strUrl: writtenCard.backgroundImgURL.url, with: "") self.contentLabel.typography = writtenCard.font == .pretendard ? .som.body3WithRegular : .som.schoolBody1WithLight self.contentLabel.text = writtenCard.content self.contentLabel.textAlignment = .center diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowerViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowerViewCell.swift index 72ddae61..43688824 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowerViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowerViewCell.swift @@ -118,7 +118,7 @@ class MyFollowerViewCell: UITableViewCell { func setModel(_ follow: Follow) { if let url = follow.backgroundImgURL?.url { - self.profileImageView.setImage(strUrl: url) + self.profileImageView.setImage(strUrl: url, with: "") } else { self.profileImageView.image = .init(.image(.defaultStyle(.sooumLogo))) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowingViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowingViewCell.swift index d674de2e6..d4b6b1bc 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowingViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/MyFollowingViewCell.swift @@ -132,7 +132,7 @@ class MyFollowingViewCell: UITableViewCell { func setModel(_ follow: Follow) { if let url = follow.backgroundImgURL?.url { - self.profileImageView.setImage(strUrl: url) + self.profileImageView.setImage(strUrl: url, with: "") } else { self.profileImageView.image = .init(.image(.defaultStyle(.sooumLogo))) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/OtherFollowViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/OtherFollowViewCell.swift index 7c32871d..b5584621 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/OtherFollowViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/Cells/OtherFollowViewCell.swift @@ -118,7 +118,7 @@ class OtherFollowViewCell: UITableViewCell { func setModel(_ follow: Follow) { if let url = follow.backgroundImgURL?.url { - self.profileImageView.setImage(strUrl: url) + self.profileImageView.setImage(strUrl: url, with: "") } else { self.profileImageView.image = .init(.image(.defaultStyle(.sooumLogo))) } diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift index 1a3f38bc..3c41963c 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Follow/FollowViewReactor.swift @@ -220,9 +220,9 @@ extension FollowViewReactor { ProfileViewReactor(provider: self.provider, type: type, memberId: memberId) } - func reactorForMainTabBar() -> MainTabBarReactor { - MainTabBarReactor(provider: self.provider) - } + // func reactorForMainTabBar() -> MainTabBarReactor { + // MainTabBarReactor(provider: self.provider) + // } } extension FollowViewReactor { diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift index e8ab544b..e4b013ba 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/ProfileViewController.swift @@ -148,11 +148,12 @@ class ProfileViewController: BaseNavigationViewController, View { self.backButton.rx.tap .subscribe(with: self) { object, _ in - if object.isBlocked { - object.navigationPop(to: MainHomeTabBarController.self, animated: true) - } else { - object.navigationPop() - } + // if object.isBlocked { + // object.navigationPop(to: MainHomeTabBarController.self, animated: true) + // } else { + // object.navigationPop() + // } + object.navigationPop() } .disposed(by: self.disposeBag) diff --git a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/CommentHistory/Cells/CommentHistoryViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/CommentHistory/Cells/CommentHistoryViewCell.swift index 0209280b..207585df 100644 --- a/SOOUM/SOOUM/Presentations/Main/Profile/Settings/CommentHistory/Cells/CommentHistoryViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Profile/Settings/CommentHistory/Cells/CommentHistoryViewCell.swift @@ -63,7 +63,7 @@ class CommentHistoryViewCell: UICollectionViewCell { } func setModel(_ strUrl: String, content: String) { - self.backgroundImageView.setImage(strUrl: strUrl) + self.backgroundImageView.setImage(strUrl: strUrl, with: "") self.contentLabel.text = content } } diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/TagDetail/TagDetailViewController.swift b/SOOUM/SOOUM/Presentations/Main/Tags/TagDetail/TagDetailViewController.swift index f7ccccbf..8d57e9db 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/TagDetail/TagDetailViewController.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/TagDetail/TagDetailViewController.swift @@ -25,8 +25,8 @@ class TagDetailViewController: BaseViewController, View { $0.separatorStyle = .none $0.register( - MainHomeViewCell.self, - forCellReuseIdentifier: String(describing: MainHomeViewCell.self) + HomeViewCell.self, + forCellReuseIdentifier: String(describing: HomeViewCell.self) ) $0.register( EmptyTagDetailTableViewCell.self, @@ -144,11 +144,11 @@ extension TagDetailViewController: UITableViewDataSource, UITableViewDelegate { return createMainHomeViewCell(indexPath: indexPath) } - private func createMainHomeViewCell(indexPath: IndexPath) -> MainHomeViewCell { - let cell: MainHomeViewCell = tableView.dequeueReusableCell( - withIdentifier: String(describing: MainHomeViewCell.self), + private func createMainHomeViewCell(indexPath: IndexPath) -> HomeViewCell { + let cell: HomeViewCell = tableView.dequeueReusableCell( + withIdentifier: String(describing: HomeViewCell.self), for: indexPath - ) as! MainHomeViewCell + ) as! HomeViewCell guard let reactor = self.reactor, reactor.currentState.tagCards.indices.contains(indexPath.row) else { return cell diff --git a/SOOUM/SOOUM/Presentations/Main/Tags/Tags/Cells/FavoriteTag/Cells/TagPreviewCard/TagPreviewCardCollectionViewCell.swift b/SOOUM/SOOUM/Presentations/Main/Tags/Tags/Cells/FavoriteTag/Cells/TagPreviewCard/TagPreviewCardCollectionViewCell.swift index 1c9e05f6..ab23ad15 100644 --- a/SOOUM/SOOUM/Presentations/Main/Tags/Tags/Cells/FavoriteTag/Cells/TagPreviewCard/TagPreviewCardCollectionViewCell.swift +++ b/SOOUM/SOOUM/Presentations/Main/Tags/Tags/Cells/FavoriteTag/Cells/TagPreviewCard/TagPreviewCardCollectionViewCell.swift @@ -32,7 +32,7 @@ class TagPreviewCardCollectionViewCell: UICollectionViewCell { } func setData(previewCard: FavoriteTagsResponse.PreviewCard) { - tagPreviewCardView.rootContainerImageView.setImage(strUrl: previewCard.backgroundImgURL.href) + tagPreviewCardView.rootContainerImageView.setImage(strUrl: previewCard.backgroundImgURL.href, with: "") tagPreviewCardView.cardTextContentLabel.text = previewCard.content } diff --git a/SOOUM/SOOUM/Resources/Alamofire/Request/CardRequest.swift b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift similarity index 96% rename from SOOUM/SOOUM/Resources/Alamofire/Request/CardRequest.swift rename to SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift index fb3167a9..b0da867a 100644 --- a/SOOUM/SOOUM/Resources/Alamofire/Request/CardRequest.swift +++ b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/CardRequest.swift @@ -60,19 +60,19 @@ enum CardRequest: BaseRequest { switch self { case let .latestCard(lastId, _, _): if let lastId = lastId { - return "/cards/home/latest/\(lastId)" + return "/api/cards/feeds/latest/\(lastId)" } else { - return "/cards/home/latest" + return "/api/cards/feeds/latest" } case .popularCard: - return "/cards/home/popular" + return "/api/cards/feeds/popular" case let .distancCard(lastId, _, _, _): if let lastId = lastId{ - return "/cards/home/distance/\(lastId)" + return "/api/cards/feeds/distance/\(lastId)" } else { - return "/cards/home/distance" + return "/api/cards/feeds/distance" } case let .detailCard(id, _, _): @@ -131,7 +131,7 @@ enum CardRequest: BaseRequest { } case let .distancCard(_, latitude, longitude, distanceFilter): - return ["latitude": latitude, "longitude": longitude, "distanceFilter": distanceFilter] + return ["latitude": latitude, "longitude": longitude, "distance": distanceFilter] case let .detailCard(_, latitude, longitude): if let latitude = latitude, let longitude = longitude { diff --git a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/NotificationRequest.swift b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/NotificationRequest.swift index 005d131c..556722ee 100644 --- a/SOOUM/SOOUM/Resources/Alamofire/Request/V2/NotificationRequest.swift +++ b/SOOUM/SOOUM/Resources/Alamofire/Request/V2/NotificationRequest.swift @@ -12,22 +12,12 @@ enum NotificationRequest: BaseRequest { /// 읽지 않은 알림 전체 조회 case unreadNotifications(lastId: String?) - /// 읽지 않은 카드 알림 조회 - case unreadCardNotifications(lastId: String?) - /// 읽지 않은 팔로우 알림 조회 - case unreadFollowNotifications(lastId: String?) - /// 읽지 않은 공지 알림 조회 - case unreadNoticeNoticeNotifications(lastId: String?) /// 읽은 알림 전체 조회 case readNotifications(lastId: String?) - /// 읽은 카드 알림 조회 - case readCardNotifications(lastId: String?) - /// 읽은 팔로우 알림 조회 - case readFollowNotifications(lastId: String?) - /// 읽은 공지 알림 조회 - case readNoticeNoticeNotifications(lastId: String?) /// 알림 읽음 요청 case requestRead(notificationId: String) + /// 공지 조회 + case notices(lastId: String?) var path: String { switch self { @@ -38,27 +28,6 @@ enum NotificationRequest: BaseRequest { return "/api/notifications/unread" } - case let .unreadCardNotifications(lastId): - if let lastId = lastId { - return "/api/notifications/unread/card/\(lastId)" - } else { - return "/api/notifications/unread/card" - } - - case let .unreadFollowNotifications(lastId): - if let lastId = lastId { - return "/api/notifications/unread/follow/\(lastId)" - } else { - return "/api/notifications/unread/follow" - } - - case let .unreadNoticeNoticeNotifications(lastId): - if let lastId = lastId { - return "/api/notifications/unread/notice/\(lastId)" - } else { - return "/api/notifications/unread/notice" - } - case let .readNotifications(lastId): if let lastId = lastId { return "/api/notifications/read/\(lastId)" @@ -66,29 +35,15 @@ enum NotificationRequest: BaseRequest { return "/api/notifications/read" } - case let .readCardNotifications(lastId): - if let lastId = lastId { - return "/api/notifications/read/card/\(lastId)" - } else { - return "/api/notifications/read/card" - } - - case let .readFollowNotifications(lastId): - if let lastId = lastId { - return "/api/notifications/read/follow/\(lastId)" - } else { - return "/api/notifications/read/follow" - } + case let .requestRead(notificationId): + return "/api/notifications/\(notificationId)/read" - case let .readNoticeNoticeNotifications(lastId): + case let .notices(lastId): if let lastId = lastId { - return "/api/notifications/read/notice/\(lastId)" + return "/api/notices/\(lastId)" } else { - return "/api/notifications/read/notice" + return "/api/notices" } - - case let .requestRead(notificationId): - return "/api/notifications/\(notificationId)/read" } } diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_bomb_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_bomb_filled.imageset/Contents.json new file mode 100644 index 00000000..175093d5 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_bomb_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_bomb_filled.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/Filled/v2_bomb_filled.imageset/v2_bomb_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_bomb_filled.imageset/v2_bomb_filled.svg new file mode 100644 index 00000000..598fed4b --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_bomb_filled.imageset/v2_bomb_filled.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_card_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_card_filled.imageset/Contents.json new file mode 100644 index 00000000..4b20aff6 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_card_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_card_filled.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/Filled/v2_card_filled.imageset/v2_card_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_card_filled.imageset/v2_card_filled.svg new file mode 100644 index 00000000..d7957eaa --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_card_filled.imageset/v2_card_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_lock_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_lock_filled.imageset/Contents.json new file mode 100644 index 00000000..8b2af3a6 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_lock_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_lock_filled.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/Filled/v2_lock_filled.imageset/v2_lock_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_lock_filled.imageset/v2_lock_filled.svg new file mode 100644 index 00000000..1d0b392c --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_lock_filled.imageset/v2_lock_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_mail_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_mail_filled.imageset/Contents.json new file mode 100644 index 00000000..d0d11681 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_mail_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_mail_filled.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/Filled/v2_mail_filled.imageset/v2_mail_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_mail_filled.imageset/v2_mail_filled.svg new file mode 100644 index 00000000..9d46a7de --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_mail_filled.imageset/v2_mail_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_notice_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_notice_filled.imageset/Contents.json new file mode 100644 index 00000000..3546ec6e --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_notice_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_notice_filled.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/Filled/v2_notice_filled.imageset/v2_notice_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_notice_filled.imageset/v2_notice_filled.svg new file mode 100644 index 00000000..b3e8dfd4 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_notice_filled.imageset/v2_notice_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_official_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_official_filled.imageset/Contents.json new file mode 100644 index 00000000..c600a588 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_official_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_official_filled.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/Filled/v2_official_filled.imageset/v2_official_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_official_filled.imageset/v2_official_filled.svg new file mode 100644 index 00000000..d48dff46 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_official_filled.imageset/v2_official_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/Contents.json index aef7b5f8..ac1f327b 100644 --- a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/Contents.json +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "tag_filled.svg", + "filename" : "v2_tag_filled.svg", "idiom" : "universal" } ], diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/tag_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/tag_filled.svg deleted file mode 100644 index d3093acd..00000000 --- a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/tag_filled.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/v2_tag_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/v2_tag_filled.svg new file mode 100644 index 00000000..fad666d7 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tag_filled.imageset/v2_tag_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tool_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tool_filled.imageset/Contents.json new file mode 100644 index 00000000..5690862e --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tool_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_tool_filled.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/Filled/v2_tool_filled.imageset/v2_tool_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tool_filled.imageset/v2_tool_filled.svg new file mode 100644 index 00000000..5adc5336 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_tool_filled.imageset/v2_tool_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_users_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_users_filled.imageset/Contents.json new file mode 100644 index 00000000..a8f88f06 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_users_filled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_users_filled.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/Filled/v2_users_filled.imageset/v2_users_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_users_filled.imageset/v2_users_filled.svg new file mode 100644 index 00000000..32cfd80d --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_users_filled.imageset/v2_users_filled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/Contents.json index 4295b2a5..233021fa 100644 --- a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/Contents.json +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "write_filled.svg", + "filename" : "v2_write_filled.svg", "idiom" : "universal" } ], diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/v2_write_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/v2_write_filled.svg new file mode 100644 index 00000000..5e9faec4 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/v2_write_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/write_filled.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/write_filled.svg deleted file mode 100644 index d4ada833..00000000 --- a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Icons/Filled/v2_write_filled.imageset/write_filled.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_home.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_home.imageset/Contents.json new file mode 100644 index 00000000..386dd753 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_home.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_placeholder.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_home.imageset/v2_placeholder.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_home.imageset/v2_placeholder.svg new file mode 100644 index 00000000..d6642707 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_home.imageset/v2_placeholder.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_notification.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_notification.imageset/Contents.json new file mode 100644 index 00000000..d8ef08aa --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_notification.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_placeholder_notification.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_notification.imageset/v2_placeholder_notification.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_notification.imageset/v2_placeholder_notification.svg new file mode 100644 index 00000000..42bbd340 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Images/v2_placeholder_notification.imageset/v2_placeholder_notification.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Logos/v2_logo_black.imageset/Contents.json b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Logos/v2_logo_black.imageset/Contents.json new file mode 100644 index 00000000..6cf38bbf --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Logos/v2_logo_black.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "v2_logo_black.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Logos/v2_logo_black.imageset/v2_logo_black.svg b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Logos/v2_logo_black.imageset/v2_logo_black.svg new file mode 100644 index 00000000..bdeabc05 --- /dev/null +++ b/SOOUM/SOOUM/Resources/Assets.xcassets/DesignSystem/V2/Logos/v2_logo_black.imageset/v2_logo_black.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/SOOUM/SOOUM/Resources/Base.lproj/LaunchScreen.storyboard b/SOOUM/SOOUM/Resources/Base.lproj/LaunchScreen.storyboard index 865e9329..bb416cf3 100644 --- a/SOOUM/SOOUM/Resources/Base.lproj/LaunchScreen.storyboard +++ b/SOOUM/SOOUM/Resources/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,9 @@ - - + + + - + + @@ -11,10 +13,23 @@ - + - + + + + + + + + + + + + + + @@ -22,4 +37,7 @@ + + + diff --git a/SOOUM/SOOUM/Resources/refrech_control_lottie.json b/SOOUM/SOOUM/Resources/refrech_control_lottie.json new file mode 100644 index 00000000..2ac24e1b --- /dev/null +++ b/SOOUM/SOOUM/Resources/refrech_control_lottie.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":44,"w":44,"meta":{"g":"@lottiefiles/creator 1.47.3"},"layers":[{"ty":4,"nm":"Shape Layer 1","sr":1,"st":1,"op":33,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[19.33671903610231,19.528985500335693]},"s":{"a":0,"k":[40,40]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[29.734687614440922,29.811594200134277]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[60,60]}},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.561,"y":0.016},"i":{"x":0.439,"y":1.017},"s":[100],"t":-1.00000004073083},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":1},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[15],"t":6},{"s":[100],"t":28}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":0.68,"y":0.19},"s":[0],"t":0},{"o":{"x":0.55,"y":0.06},"i":{"x":1,"y":1},"s":[0],"t":6},{"o":{"x":0,"y":0},"i":{"x":0.36,"y":1},"s":[90],"t":28},{"s":[99],"t":33}],"ix":1},"m":1},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":12},"c":{"a":0,"k":[0.1255,0.7765,0.9255]}}],"ind":1},{"ty":0,"nm":"Nested Scene 1","sr":1,"st":0,"op":33,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[30,30]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[22,22]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":60,"h":60,"refId":"precomp_newScene_1beb0f4e-74e1-40f2-b77d-ab5a4d6c6cb4","ind":2}],"v":"5.7.0","fr":29.9700012207031,"op":33,"ip":0,"assets":[{"nm":"Nested Scene 1","id":"precomp_newScene_1beb0f4e-74e1-40f2-b77d-ab5a4d6c6cb4","fr":29.9700012207031,"layers":[{"ty":4,"nm":"Ellipse 1","sr":1,"st":0,"op":33,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[40,40]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[30,30]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":20}},"shapes":[{"ty":"el","bm":0,"hd":false,"nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[60,60]}},{"ty":"st","bm":0,"hd":false,"nm":"Stroke","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100},"w":{"a":0,"k":12},"c":{"a":0,"k":[0.1255,0.7765,0.9255]}}],"ind":1}]}]} \ No newline at end of file