diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..3356193 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index e26594c..6b96557 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ iOSInjectionProject/ # End of https://www.toptal.com/developers/gitignore/api/swift,xcode On_off_iOS/.DS_Store + +On_off_iOS/On_off_iOS/Config.xcconfig diff --git a/On_off_iOS/On_off_iOS.xcodeproj/project.pbxproj b/On_off_iOS/On_off_iOS.xcodeproj/project.pbxproj index 87005b2..38f7570 100644 --- a/On_off_iOS/On_off_iOS.xcodeproj/project.pbxproj +++ b/On_off_iOS/On_off_iOS.xcodeproj/project.pbxproj @@ -48,7 +48,6 @@ 374FD49A2B42690D00F2E645 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 374FD4992B42690D00F2E645 /* SnapKit */; }; 374FD49D2B4281E100F2E645 /* OnboardingCustomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374FD49C2B4281E100F2E645 /* OnboardingCustomView.swift */; }; 374FD49F2B42825B00F2E645 /* CustomPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374FD49E2B42825B00F2E645 /* CustomPageControl.swift */; }; - 374FD4A22B4294F100F2E645 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374FD4A12B4294F100F2E645 /* LoginViewController.swift */; }; 374FD4A42B4297BE00F2E645 /* OnBoardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374FD4A32B4297BE00F2E645 /* OnBoardingViewController.swift */; }; 3769A6862B58563000D79C33 /* SelectTimeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769A6852B58563000D79C33 /* SelectTimeViewModel.swift */; }; 3769A6882B58563C00D79C33 /* SelectTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769A6872B58563C00D79C33 /* SelectTimeViewController.swift */; }; @@ -60,11 +59,66 @@ 378140612B42E83100F2AA5A /* NickNameViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378140602B42E83100F2AA5A /* NickNameViewModel.swift */; }; 378140632B42F07A00F2AA5A /* ProfileSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378140622B42F07A00F2AA5A /* ProfileSettingViewController.swift */; }; 378140662B42F0AB00F2AA5A /* ProfileSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378140652B42F0AB00F2AA5A /* ProfileSettingViewModel.swift */; }; + 378588EF2B707EED004E262F /* ModalSelectProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378588EE2B707EED004E262F /* ModalSelectProfileViewController.swift */; }; + 378588F22B707F0A004E262F /* ModalSelectProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378588F12B707F0A004E262F /* ModalSelectProfileViewModel.swift */; }; + 378588F72B708701004E262F /* ProfileDataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378588F62B708701004E262F /* ProfileDataType.swift */; }; + 378588F92B708F99004E262F /* ModalSelectProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378588F82B708F99004E262F /* ModalSelectProfileTableViewCell.swift */; }; + 378588FB2B7092F7004E262F /* ModalSelectProfileDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378588FA2B7092F7004E262F /* ModalSelectProfileDelegate.swift */; }; 3787D0082B429C1100F054DD /* OnboardingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787D0072B429C1100F054DD /* OnboardingModel.swift */; }; 3787D00A2B429F1700F054DD /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787D0092B429F1700F054DD /* LaunchViewController.swift */; }; 3787D00F2B42AD6F00F054DD /* OnBoardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787D00E2B42AD6F00F054DD /* OnBoardingViewModel.swift */; }; - 3787D0112B42E0B100F054DD /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787D0102B42E0B100F054DD /* LoginViewModel.swift */; }; 3787D0132B42E0F000F054DD /* NickNameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787D0122B42E0F000F054DD /* NickNameViewController.swift */; }; + 37A1E9862B6C821A0013FFD7 /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 37A1E9852B6C821A0013FFD7 /* RxGesture */; }; + 37A1E9882B6CB1DD0013FFD7 /* LoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A1E9872B6CB1DD0013FFD7 /* LoginService.swift */; }; + 37A1E98A2B6CB1F70013FFD7 /* LoginProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A1E9892B6CB1F70013FFD7 /* LoginProtocol.swift */; }; + 37A1E98D2B6CCC020013FFD7 /* Memoirs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A1E98C2B6CCC020013FFD7 /* Memoirs.swift */; }; + 37A1E9942B6CDAD70013FFD7 /* ModalEmoticonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A1E9932B6CDAD70013FFD7 /* ModalEmoticonViewController.swift */; }; + 37A1E9972B6CDCEF0013FFD7 /* EmoticonCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A1E9962B6CDCEF0013FFD7 /* EmoticonCollectionViewCell.swift */; }; + 37AEB1EC2B6CEE0E00A6CDD6 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 37AEB1EB2B6CEE0E00A6CDD6 /* Kingfisher */; }; + 37AEB1EE2B6CF3F400A6CDD6 /* ModalEmoticonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AEB1ED2B6CF3F400A6CDD6 /* ModalEmoticonViewModel.swift */; }; + 37AEB1F22B6CFE3500A6CDD6 /* MemoirsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AEB1F12B6CFE3500A6CDD6 /* MemoirsService.swift */; }; + 37AEB1F52B6CFE6C00A6CDD6 /* MemoirsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AEB1F42B6CFE6C00A6CDD6 /* MemoirsProtocol.swift */; }; + 37B890742B6A1ECD008A8BBC /* Domain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B890732B6A1ECD008A8BBC /* Domain.swift */; }; + 37B890762B6A1EDC008A8BBC /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B890752B6A1EDC008A8BBC /* Endpoint.swift */; }; + 37BD17702B764B2800A4EA46 /* LaunchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD176F2B764B2800A4EA46 /* LaunchViewModel.swift */; }; + 37C161232B6BD9F8000E0B21 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C161202B6BD9F7000E0B21 /* LoginViewModel.swift */; }; + 37C161242B6BD9F8000E0B21 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C161222B6BD9F7000E0B21 /* LoginViewController.swift */; }; + 37C161262B6BDAD1000E0B21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 37C161252B6BDAD0000E0B21 /* GoogleService-Info.plist */; }; + 37C161292B6BDECF000E0B21 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161282B6BDECF000E0B21 /* Alamofire */; }; + 37C1612C2B6BDEF8000E0B21 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C1612B2B6BDEF8000E0B21 /* Header.swift */; }; + 37C161312B6BDF59000E0B21 /* KeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C161302B6BDF59000E0B21 /* KeychainWrapper.swift */; }; + 37C1613A2B6BE43A000E0B21 /* SignUpProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C161392B6BE43A000E0B21 /* SignUpProtocol.swift */; }; + 37C1613C2B6BE519000E0B21 /* Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C1613B2B6BE519000E0B21 /* Login.swift */; }; + 37C161402B6BE77A000E0B21 /* KakaoSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 37C1613F2B6BE77A000E0B21 /* KakaoSDK */; }; + 37C161422B6BE77A000E0B21 /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161412B6BE77A000E0B21 /* KakaoSDKAuth */; }; + 37C161442B6BE77A000E0B21 /* KakaoSDKCert in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161432B6BE77A000E0B21 /* KakaoSDKCert */; }; + 37C161462B6BE77A000E0B21 /* KakaoSDKCertCore in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161452B6BE77A000E0B21 /* KakaoSDKCertCore */; }; + 37C161482B6BE77A000E0B21 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161472B6BE77A000E0B21 /* KakaoSDKCommon */; }; + 37C1614A2B6BE77A000E0B21 /* KakaoSDKFriend in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161492B6BE77A000E0B21 /* KakaoSDKFriend */; }; + 37C1614C2B6BE77A000E0B21 /* KakaoSDKFriendCore in Frameworks */ = {isa = PBXBuildFile; productRef = 37C1614B2B6BE77A000E0B21 /* KakaoSDKFriendCore */; }; + 37C1614E2B6BE77A000E0B21 /* KakaoSDKNavi in Frameworks */ = {isa = PBXBuildFile; productRef = 37C1614D2B6BE77A000E0B21 /* KakaoSDKNavi */; }; + 37C161502B6BE77A000E0B21 /* KakaoSDKShare in Frameworks */ = {isa = PBXBuildFile; productRef = 37C1614F2B6BE77A000E0B21 /* KakaoSDKShare */; }; + 37C161522B6BE77A000E0B21 /* KakaoSDKTalk in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161512B6BE77A000E0B21 /* KakaoSDKTalk */; }; + 37C161542B6BE77A000E0B21 /* KakaoSDKTemplate in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161532B6BE77A000E0B21 /* KakaoSDKTemplate */; }; + 37C161562B6BE77A000E0B21 /* KakaoSDKUser in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161552B6BE77A000E0B21 /* KakaoSDKUser */; }; + 37C1615D2B6BEDED000E0B21 /* RxKakaoSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 37C1615C2B6BEDED000E0B21 /* RxKakaoSDK */; }; + 37C1615F2B6BEDED000E0B21 /* RxKakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 37C1615E2B6BEDED000E0B21 /* RxKakaoSDKAuth */; }; + 37C161612B6BEDED000E0B21 /* RxKakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161602B6BEDED000E0B21 /* RxKakaoSDKCommon */; }; + 37C161632B6BEDED000E0B21 /* RxKakaoSDKFriend in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161622B6BEDED000E0B21 /* RxKakaoSDKFriend */; }; + 37C161652B6BEDED000E0B21 /* RxKakaoSDKShare in Frameworks */ = {isa = PBXBuildFile; productRef = 37C161642B6BEDED000E0B21 /* RxKakaoSDKShare */; }; + 37C70B422B714C8B004CB0F6 /* Ext + NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C70B412B714C8B004CB0F6 /* Ext + NSAttributedString.swift */; }; + 37CC77752B6F9620007999E5 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC77742B6F9620007999E5 /* KeyChain.swift */; }; + 37CC77792B6FA864007999E5 /* ModalEmoticonDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC77782B6FA864007999E5 /* ModalEmoticonDelegate.swift */; }; + 37CFD3002B66533900E00D92 /* ExpressedIconViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CFD2FF2B66533900E00D92 /* ExpressedIconViewController.swift */; }; + 37CFD3022B66534400E00D92 /* ExpressedIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CFD3012B66534400E00D92 /* ExpressedIconViewModel.swift */; }; + 37D606FD2B7783E20092F113 /* LaunchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D606FC2B7783E20092F113 /* LaunchModel.swift */; }; + 37DEF20D2B66B31D00AF47DB /* MemoirsCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DEF20C2B66B31D00AF47DB /* MemoirsCompleteViewController.swift */; }; + 37DEF20F2B66B32A00AF47DB /* MemoirsCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DEF20E2B66B32A00AF47DB /* MemoirsCompleteViewModel.swift */; }; + 37FED6032B6E15000071BB23 /* MemoirsImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FED6022B6E15000071BB23 /* MemoirsImage.swift */; }; + 37FED6072B6E23560071BB23 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FED6062B6E23560071BB23 /* BookmarkViewController.swift */; }; + 37FED6092B6E235D0071BB23 /* BookmarkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FED6082B6E235D0071BB23 /* BookmarkViewModel.swift */; }; + 37FED60B2B6E23650071BB23 /* BookmarkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FED60A2B6E23650071BB23 /* BookmarkService.swift */; }; + 37FED60D2B6E29070071BB23 /* BookmarkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FED60C2B6E29070071BB23 /* BookmarkTableViewCell.swift */; }; 3B0129C32B623BB900B191AD /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0129C22B623BB900B191AD /* HomeViewController.swift */; }; 3B0129C62B6242F800B191AD /* Ext + ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0129C52B6242F800B191AD /* Ext + ViewController.swift */; }; 3B0129C82B6246E900B191AD /* Ext + UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0129C72B6246E900B191AD /* Ext + UIImage.swift */; }; @@ -130,7 +184,6 @@ 35E9BE922B7B15C300ECAF80 /* WorkLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkLogTableViewCell.swift; sourceTree = ""; }; 374FD49C2B4281E100F2E645 /* OnboardingCustomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCustomView.swift; sourceTree = ""; }; 374FD49E2B42825B00F2E645 /* CustomPageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPageControl.swift; sourceTree = ""; }; - 374FD4A12B4294F100F2E645 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 374FD4A32B4297BE00F2E645 /* OnBoardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingViewController.swift; sourceTree = ""; }; 3769A6852B58563000D79C33 /* SelectTimeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTimeViewModel.swift; sourceTree = ""; }; 3769A6872B58563C00D79C33 /* SelectTimeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTimeViewController.swift; sourceTree = ""; }; @@ -141,11 +194,48 @@ 378140602B42E83100F2AA5A /* NickNameViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NickNameViewModel.swift; sourceTree = ""; }; 378140622B42F07A00F2AA5A /* ProfileSettingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingViewController.swift; sourceTree = ""; }; 378140652B42F0AB00F2AA5A /* ProfileSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingViewModel.swift; sourceTree = ""; }; + 378588EE2B707EED004E262F /* ModalSelectProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSelectProfileViewController.swift; sourceTree = ""; }; + 378588F12B707F0A004E262F /* ModalSelectProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSelectProfileViewModel.swift; sourceTree = ""; }; + 378588F62B708701004E262F /* ProfileDataType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDataType.swift; sourceTree = ""; }; + 378588F82B708F99004E262F /* ModalSelectProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSelectProfileTableViewCell.swift; sourceTree = ""; }; + 378588FA2B7092F7004E262F /* ModalSelectProfileDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSelectProfileDelegate.swift; sourceTree = ""; }; 3787D0072B429C1100F054DD /* OnboardingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModel.swift; sourceTree = ""; }; 3787D0092B429F1700F054DD /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; 3787D00E2B42AD6F00F054DD /* OnBoardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingViewModel.swift; sourceTree = ""; }; - 3787D0102B42E0B100F054DD /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 3787D0122B42E0F000F054DD /* NickNameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NickNameViewController.swift; sourceTree = ""; }; + 37A1E9832B6C7FC60013FFD7 /* On_off_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = On_off_iOS.entitlements; sourceTree = ""; }; + 37A1E9872B6CB1DD0013FFD7 /* LoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginService.swift; sourceTree = ""; }; + 37A1E9892B6CB1F70013FFD7 /* LoginProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginProtocol.swift; sourceTree = ""; }; + 37A1E98C2B6CCC020013FFD7 /* Memoirs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memoirs.swift; sourceTree = ""; }; + 37A1E9932B6CDAD70013FFD7 /* ModalEmoticonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalEmoticonViewController.swift; sourceTree = ""; }; + 37A1E9962B6CDCEF0013FFD7 /* EmoticonCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoticonCollectionViewCell.swift; sourceTree = ""; }; + 37AEB1ED2B6CF3F400A6CDD6 /* ModalEmoticonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalEmoticonViewModel.swift; sourceTree = ""; }; + 37AEB1F12B6CFE3500A6CDD6 /* MemoirsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoirsService.swift; sourceTree = ""; }; + 37AEB1F42B6CFE6C00A6CDD6 /* MemoirsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoirsProtocol.swift; sourceTree = ""; }; + 37B890732B6A1ECD008A8BBC /* Domain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Domain.swift; sourceTree = ""; }; + 37B890752B6A1EDC008A8BBC /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + 37BD176F2B764B2800A4EA46 /* LaunchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewModel.swift; sourceTree = ""; }; + 37C161202B6BD9F7000E0B21 /* LoginViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + 37C161222B6BD9F7000E0B21 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + 37C161252B6BDAD0000E0B21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 37C1612B2B6BDEF8000E0B21 /* Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Header.swift; sourceTree = ""; }; + 37C161302B6BDF59000E0B21 /* KeychainWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainWrapper.swift; sourceTree = ""; }; + 37C161392B6BE43A000E0B21 /* SignUpProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpProtocol.swift; sourceTree = ""; }; + 37C1613B2B6BE519000E0B21 /* Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Login.swift; sourceTree = ""; }; + 37C1613D2B6BE6B3000E0B21 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + 37C70B412B714C8B004CB0F6 /* Ext + NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Ext + NSAttributedString.swift"; sourceTree = ""; }; + 37CC77742B6F9620007999E5 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = ""; }; + 37CC77782B6FA864007999E5 /* ModalEmoticonDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalEmoticonDelegate.swift; sourceTree = ""; }; + 37CFD2FF2B66533900E00D92 /* ExpressedIconViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpressedIconViewController.swift; sourceTree = ""; }; + 37CFD3012B66534400E00D92 /* ExpressedIconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpressedIconViewModel.swift; sourceTree = ""; }; + 37D606FC2B7783E20092F113 /* LaunchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchModel.swift; sourceTree = ""; }; + 37DEF20C2B66B31D00AF47DB /* MemoirsCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoirsCompleteViewController.swift; sourceTree = ""; }; + 37DEF20E2B66B32A00AF47DB /* MemoirsCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoirsCompleteViewModel.swift; sourceTree = ""; }; + 37FED6022B6E15000071BB23 /* MemoirsImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoirsImage.swift; sourceTree = ""; }; + 37FED6062B6E23560071BB23 /* BookmarkViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewController.swift; sourceTree = ""; }; + 37FED6082B6E235D0071BB23 /* BookmarkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; + 37FED60A2B6E23650071BB23 /* BookmarkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkService.swift; sourceTree = ""; }; + 37FED60C2B6E29070071BB23 /* BookmarkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkTableViewCell.swift; sourceTree = ""; }; 3B0129C22B623BB900B191AD /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 3B0129C52B6242F800B191AD /* Ext + ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Ext + ViewController.swift"; sourceTree = ""; }; 3B0129C72B6246E900B191AD /* Ext + UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Ext + UIImage.swift"; sourceTree = ""; }; @@ -173,13 +263,26 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 37C1614C2B6BE77A000E0B21 /* KakaoSDKFriendCore in Frameworks */, + 37C161632B6BEDED000E0B21 /* RxKakaoSDKFriend in Frameworks */, 376A4B5C2B5506D7004FBB56 /* Lottie in Frameworks */, 358CD9F02B7B81BB00B6EF85 /* Alamofire in Frameworks */, 374FD4972B4268EC00F2E645 /* RxSwift in Frameworks */, + 37C161482B6BE77A000E0B21 /* KakaoSDKCommon in Frameworks */, + 37C1615D2B6BEDED000E0B21 /* RxKakaoSDK in Frameworks */, + 37A1E9862B6C821A0013FFD7 /* RxGesture in Frameworks */, 374FD49A2B42690D00F2E645 /* SnapKit in Frameworks */, + 37C161522B6BE77A000E0B21 /* KakaoSDKTalk in Frameworks */, 374FD4952B4268EC00F2E645 /* RxRelay in Frameworks */, + 37C161502B6BE77A000E0B21 /* KakaoSDKShare in Frameworks */, 374FD4932B4268EC00F2E645 /* RxCocoa in Frameworks */, + 37C1615F2B6BEDED000E0B21 /* RxKakaoSDKAuth in Frameworks */, 3B8570CB2B582B4E000BC503 /* FSCalendar in Frameworks */, + 37C161422B6BE77A000E0B21 /* KakaoSDKAuth in Frameworks */, + 37C1614E2B6BE77A000E0B21 /* KakaoSDKNavi in Frameworks */, + 37C161652B6BEDED000E0B21 /* RxKakaoSDKShare in Frameworks */, + 37C161462B6BE77A000E0B21 /* KakaoSDKCertCore in Frameworks */, + 37C161442B6BE77A000E0B21 /* KakaoSDKCert in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -364,15 +467,6 @@ path = OnBoarding; sourceTree = ""; }; - 374FD4A02B4294DE00F2E645 /* LogIn */ = { - isa = PBXGroup; - children = ( - 3787D0152B42E5AC00F054DD /* View */, - 3787D0142B42E5A500F054DD /* ViewModel */, - ); - path = LogIn; - sourceTree = ""; - }; 3769A6842B58561D00D79C33 /* SelectTime */ = { isa = PBXGroup; children = ( @@ -438,6 +532,8 @@ isa = PBXGroup; children = ( 3787D0092B429F1700F054DD /* LaunchViewController.swift */, + 37BD176F2B764B2800A4EA46 /* LaunchViewModel.swift */, + 37D606FC2B7783E20092F113 /* LaunchModel.swift */, ); path = Launch; sourceTree = ""; @@ -467,6 +563,24 @@ path = View; sourceTree = ""; }; + 378588F02B707EF9004E262F /* Modal */ = { + isa = PBXGroup; + children = ( + 378588F32B707F17004E262F /* ModalCollectionView */, + 378588EE2B707EED004E262F /* ModalSelectProfileViewController.swift */, + 378588F12B707F0A004E262F /* ModalSelectProfileViewModel.swift */, + 378588F82B708F99004E262F /* ModalSelectProfileTableViewCell.swift */, + ); + path = Modal; + sourceTree = ""; + }; + 378588F32B707F17004E262F /* ModalCollectionView */ = { + isa = PBXGroup; + children = ( + ); + path = ModalCollectionView; + sourceTree = ""; + }; 3787D00B2B42A84100F054DD /* View */ = { isa = PBXGroup; children = ( @@ -493,29 +607,190 @@ path = Model; sourceTree = ""; }; - 3787D0142B42E5A500F054DD /* ViewModel */ = { + 379380CC2B44282B00BB7BDE /* ProfileSetting */ = { + isa = PBXGroup; + children = ( + 3769A68A2B58566400D79C33 /* View */, + 3769A68B2B58566B00D79C33 /* ViewModel */, + ); + path = ProfileSetting; + sourceTree = ""; + }; + 37A1E98B2B6CCBE70013FFD7 /* Model */ = { + isa = PBXGroup; + children = ( + 37A1E98C2B6CCC020013FFD7 /* Memoirs.swift */, + ); + path = Model; + sourceTree = ""; + }; + 37A1E9922B6CD8B70013FFD7 /* Emoticon */ = { + isa = PBXGroup; + children = ( + 37A1E9952B6CDCE50013FFD7 /* CollectionView */, + 37A1E9932B6CDAD70013FFD7 /* ModalEmoticonViewController.swift */, + 37AEB1ED2B6CF3F400A6CDD6 /* ModalEmoticonViewModel.swift */, + ); + path = Emoticon; + sourceTree = ""; + }; + 37A1E9952B6CDCE50013FFD7 /* CollectionView */ = { + isa = PBXGroup; + children = ( + 37A1E9962B6CDCEF0013FFD7 /* EmoticonCollectionViewCell.swift */, + ); + path = CollectionView; + sourceTree = ""; + }; + 37AEB1F32B6CFE6000A6CDD6 /* Protocol */ = { + isa = PBXGroup; + children = ( + 37AEB1F42B6CFE6C00A6CDD6 /* MemoirsProtocol.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 37B890712B6A1EAD008A8BBC /* Constants */ = { + isa = PBXGroup; + children = ( + 3B51B5262B63DF9500E5FEF9 /* CellIdentifier.swift */, + 37B890722B6A1EBC008A8BBC /* API */, + 37C1612B2B6BDEF8000E0B21 /* Header.swift */, + ); + path = Constants; + sourceTree = ""; + }; + 37B890722B6A1EBC008A8BBC /* API */ = { + isa = PBXGroup; + children = ( + 37B890732B6A1ECD008A8BBC /* Domain.swift */, + 37B890752B6A1EDC008A8BBC /* Endpoint.swift */, + ); + path = API; + sourceTree = ""; + }; + 37C1611E2B6BD9F7000E0B21 /* LogIn */ = { + isa = PBXGroup; + children = ( + 379380CC2B44282B00BB7BDE /* ProfileSetting */, + 3781405F2B42E75000F2AA5A /* NickName */, + 378588F02B707EF9004E262F /* Modal */, + 37C161382B6BE42F000E0B21 /* Protocol */, + 37C161372B6BE3EB000E0B21 /* Service */, + 37C161322B6BDFA3000E0B21 /* Model */, + 37C1611F2B6BD9F7000E0B21 /* ViewModel */, + 37C161212B6BD9F7000E0B21 /* View */, + 378588FA2B7092F7004E262F /* ModalSelectProfileDelegate.swift */, + ); + path = LogIn; + sourceTree = ""; + }; + 37C1611F2B6BD9F7000E0B21 /* ViewModel */ = { isa = PBXGroup; children = ( - 3787D0102B42E0B100F054DD /* LoginViewModel.swift */, + 37C161202B6BD9F7000E0B21 /* LoginViewModel.swift */, ); path = ViewModel; sourceTree = ""; }; - 3787D0152B42E5AC00F054DD /* View */ = { + 37C161212B6BD9F7000E0B21 /* View */ = { isa = PBXGroup; children = ( - 374FD4A12B4294F100F2E645 /* LoginViewController.swift */, + 37C161222B6BD9F7000E0B21 /* LoginViewController.swift */, ); path = View; sourceTree = ""; }; - 379380CC2B44282B00BB7BDE /* ProfileSetting */ = { + 37C1612F2B6BDF47000E0B21 /* KeyChain */ = { isa = PBXGroup; children = ( - 3769A68A2B58566400D79C33 /* View */, - 3769A68B2B58566B00D79C33 /* ViewModel */, + 37C161302B6BDF59000E0B21 /* KeychainWrapper.swift */, ); - path = ProfileSetting; + path = KeyChain; + sourceTree = ""; + }; + 37C161322B6BDFA3000E0B21 /* Model */ = { + isa = PBXGroup; + children = ( + 37C1613B2B6BE519000E0B21 /* Login.swift */, + 378588F62B708701004E262F /* ProfileDataType.swift */, + ); + path = Model; + sourceTree = ""; + }; + 37C161372B6BE3EB000E0B21 /* Service */ = { + isa = PBXGroup; + children = ( + 37A1E9872B6CB1DD0013FFD7 /* LoginService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 37C161382B6BE42F000E0B21 /* Protocol */ = { + isa = PBXGroup; + children = ( + 37C161392B6BE43A000E0B21 /* SignUpProtocol.swift */, + 37A1E9892B6CB1F70013FFD7 /* LoginProtocol.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 37CC77732B6F95F4007999E5 /* KeyChain */ = { + isa = PBXGroup; + children = ( + 37CC77742B6F9620007999E5 /* KeyChain.swift */, + ); + name = KeyChain; + path = Bookmark/KeyChain; + sourceTree = ""; + }; + 37CFD2FE2B6652DA00E00D92 /* ExpressedIcon */ = { + isa = PBXGroup; + children = ( + 37A1E9922B6CD8B70013FFD7 /* Emoticon */, + 37CFD2FF2B66533900E00D92 /* ExpressedIconViewController.swift */, + 37CFD3012B66534400E00D92 /* ExpressedIconViewModel.swift */, + 3708261A2B66962000FBCAAF /* ExpressedIconModel.swift */, + 37CC77782B6FA864007999E5 /* ModalEmoticonDelegate.swift */, + ); + path = ExpressedIcon; + sourceTree = ""; + }; + 37DEF20A2B66B2F300AF47DB /* MemoirsComplete */ = { + isa = PBXGroup; + children = ( + 37DEF20B2B66B30D00AF47DB /* ViewController */, + 37DEF20E2B66B32A00AF47DB /* MemoirsCompleteViewModel.swift */, + ); + path = MemoirsComplete; + sourceTree = ""; + }; + 37DEF20B2B66B30D00AF47DB /* ViewController */ = { + isa = PBXGroup; + children = ( + 37DEF20C2B66B31D00AF47DB /* MemoirsCompleteViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + 37FED6042B6E16490071BB23 /* MemoirsConstants */ = { + isa = PBXGroup; + children = ( + 37FED6022B6E15000071BB23 /* MemoirsImage.swift */, + 3708261E2B6699E000FBCAAF /* MemoirsText.swift */, + ); + path = MemoirsConstants; + sourceTree = ""; + }; + 37FED6052B6E23380071BB23 /* Bookmark */ = { + isa = PBXGroup; + children = ( + 37FED6062B6E23560071BB23 /* BookmarkViewController.swift */, + 37FED6082B6E235D0071BB23 /* BookmarkViewModel.swift */, + 37FED60A2B6E23650071BB23 /* BookmarkService.swift */, + 37FED60C2B6E29070071BB23 /* BookmarkTableViewCell.swift */, + ); + path = Bookmark; sourceTree = ""; }; 3B0129C12B623BA900B191AD /* Home */ = { @@ -534,6 +809,7 @@ 3B0129C72B6246E900B191AD /* Ext + UIImage.swift */, 355283112B68CB70002BBFFD /* Ext + UIFont.swift */, 355283132B68CC94002BBFFD /* Ext + UIColor.swift */, + 37C70B412B714C8B004CB0F6 /* Ext + NSAttributedString.swift */, ); path = Extension; sourceTree = ""; @@ -597,12 +873,10 @@ 358CD9E12B7B6EF200B6EF85 /* Constants */, 35984F222B63EE8000A5F6F2 /* TabBar */, 3B0129C12B623BA900B191AD /* Home */, + 371B9EA52B5AD7D8006AC06D /* Memoirs */, 3B9C89CF2B4AED640083CF44 /* Statistics */, 3769A6842B58561D00D79C33 /* SelectTime */, 376A4B5D2B5514B4004FBB56 /* Launch */, - 379380CC2B44282B00BB7BDE /* ProfileSetting */, - 3781405F2B42E75000F2AA5A /* NickName */, - 374FD4A02B4294DE00F2E645 /* LogIn */, 374FD49B2B4281CA00F2E645 /* OnBoarding */, 3B4230212B41584300D0B038 /* Apps */, 376A4B552B5504AB004FBB56 /* Assets */, @@ -610,6 +884,9 @@ 35E0C7282B68C7AC006BF427 /* Fonts */, 3B4230182B41572400D0B038 /* LaunchScreen.storyboard */, 3B42301B2B41572400D0B038 /* Info.plist */, + 37C161252B6BDAD0000E0B21 /* GoogleService-Info.plist */, + 37C1613D2B6BE6B3000E0B21 /* Config.xcconfig */, + 370826202B669BFC00FBCAAF /* UICalculate+Ex.swift */, ); path = On_off_iOS; sourceTree = ""; @@ -739,6 +1016,7 @@ files = ( 376A4B572B5504FE004FBB56 /* Lotties.xcassets in Resources */, 3B42301A2B41572400D0B038 /* LaunchScreen.storyboard in Resources */, + 37C161262B6BDAD1000E0B21 /* GoogleService-Info.plist in Resources */, 376A4B592B55054D004FBB56 /* onBoarding.json in Resources */, 3B4230172B41572400D0B038 /* Assets.xcassets in Resources */, ); @@ -764,15 +1042,23 @@ 374FD49F2B42825B00F2E645 /* CustomPageControl.swift in Sources */, 3543D0E52B6D27FB008BC111 /* DayCollectionViewCell.swift in Sources */, 355283122B68CB70002BBFFD /* Ext + UIFont.swift in Sources */, + 37FED60D2B6E29070071BB23 /* BookmarkTableViewCell.swift in Sources */, 355283142B68CC94002BBFFD /* Ext + UIColor.swift in Sources */, 359395F72B70AD07005C706C /* MenuModalViewModel.swift in Sources */, 3769A6862B58563000D79C33 /* SelectTimeViewModel.swift in Sources */, 374FD4A42B4297BE00F2E645 /* OnBoardingViewController.swift in Sources */, 35E9BE8B2B7B14F400ECAF80 /* OnUIView.swift in Sources */, 3769A6882B58563C00D79C33 /* SelectTimeViewController.swift in Sources */, + 37A1E9972B6CDCEF0013FFD7 /* EmoticonCollectionViewCell.swift in Sources */, + 37C1612C2B6BDEF8000E0B21 /* Header.swift in Sources */, 3787D0082B429C1100F054DD /* OnboardingModel.swift in Sources */, + 37C70B422B714C8B004CB0F6 /* Ext + NSAttributedString.swift in Sources */, 378140662B42F0AB00F2AA5A /* ProfileSettingViewModel.swift in Sources */, + 37C1613C2B6BE519000E0B21 /* Login.swift in Sources */, 3B1658F72B4D8559004EFBC3 /* WeekChartCustomView.swift in Sources */, + 378588EF2B707EED004E262F /* ModalSelectProfileViewController.swift in Sources */, + 37D606FD2B7783E20092F113 /* LaunchModel.swift in Sources */, + 37AEB1F22B6CFE3500A6CDD6 /* MemoirsService.swift in Sources */, 3B42300E2B41572200D0B038 /* AppDelegate.swift in Sources */, 3543D0EB2B6D2AB4008BC111 /* TodayResolution.swift in Sources */, 3769A68E2B585B6600D79C33 /* DayButton.swift in Sources */, @@ -780,13 +1066,17 @@ 3769A6912B585BDE00D79C33 /* DayModel.swift in Sources */, 35E9BE932B7B15C300ECAF80 /* WorkLogTableViewCell.swift in Sources */, 3B0129CC2B624D6000B191AD /* Home.swift in Sources */, + 371B9EBF2B5B075A006AC06D /* WriteImprovementViewController.swift in Sources */, 374FD49D2B4281E100F2E645 /* OnboardingCustomView.swift in Sources */, 3BB806D82B5024BC00555E58 /* MonthStatistics.swift in Sources */, + 37CC77752B6F9620007999E5 /* KeyChain.swift in Sources */, 3584F3AE2B5A5FEB007ACB57 /* (null) in Sources */, + 37C161312B6BDF59000E0B21 /* KeychainWrapper.swift in Sources */, 3BB806D32B5021BC00555E58 /* StatisticsViewModel.swift in Sources */, 3543D0F22B6F5FF0008BC111 /* ResolutionWriteViewController.swift in Sources */, 35E9BE7F2B7B124800ECAF80 /* OnUIViewModel.swift in Sources */, 3B0129C82B6246E900B191AD /* Ext + UIImage.swift in Sources */, + 378588F22B707F0A004E262F /* ModalSelectProfileViewModel.swift in Sources */, 3B4230102B41572200D0B038 /* SceneDelegate.swift in Sources */, 3584F3AC2B5A5CC7007ACB57 /* (null) in Sources */, 354FC0F42B72373B00F4ADEB /* AddWriteViewModel.swift in Sources */, @@ -794,6 +1084,7 @@ 358CD9E82B7B716700B6EF85 /* Header.swift in Sources */, 35E9BE8F2B7B156E00ECAF80 /* LogOptionButton.swift in Sources */, 378140632B42F07A00F2AA5A /* ProfileSettingViewController.swift in Sources */, + 37B890762B6A1EDC008A8BBC /* Endpoint.swift in Sources */, 3B0129C32B623BB900B191AD /* HomeViewController.swift in Sources */, 3787D0112B42E0B100F054DD /* LoginViewModel.swift in Sources */, 358CD9ED2B7B722800B6EF85 /* KeyChainWrapper.swift in Sources */, @@ -806,7 +1097,12 @@ 3BD552662B58FA8C0043920E /* RateFillView.swift in Sources */, 35AC0F502B64025300AB0A6B /* TabItem.swift in Sources */, 3BD5526A2B5914B10043920E /* CalendarStatistics.swift in Sources */, + 37DEF20F2B66B32A00AF47DB /* MemoirsCompleteViewModel.swift in Sources */, 3B9C89D12B4AED7C0083CF44 /* StatisticsViewController.swift in Sources */, + 37AEB1F52B6CFE6C00A6CDD6 /* MemoirsProtocol.swift in Sources */, + 37C1613A2B6BE43A000E0B21 /* SignUpProtocol.swift in Sources */, + 37A1E9942B6CDAD70013FFD7 /* ModalEmoticonViewController.swift in Sources */, + 378588FB2B7092F7004E262F /* ModalSelectProfileDelegate.swift in Sources */, 3B0129C62B6242F800B191AD /* Ext + ViewController.swift in Sources */, 35E9BE7D2B7B0F5700ECAF80 /* Worklog.swift in Sources */, 3BB806D02B5012F400555E58 /* DayChartCustomView.swift in Sources */, @@ -836,6 +1132,7 @@ /* Begin XCBuildConfiguration section */ 3B42301C2B41572400D0B038 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 37C1613D2B6BE6B3000E0B21 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -899,6 +1196,7 @@ }; 3B42301D2B41572400D0B038 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 37C1613D2B6BE6B3000E0B21 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -955,6 +1253,7 @@ }; 3B42301F2B41572400D0B038 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 37C1613D2B6BE6B3000E0B21 /* Config.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -986,6 +1285,7 @@ }; 3B4230202B41572400D0B038 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 37C1613D2B6BE6B3000E0B21 /* Config.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1072,6 +1372,46 @@ minimumVersion = 4.3.4; }; }; + 37A1E9842B6C821A0013FFD7 /* XCRemoteSwiftPackageReference "RxGesture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/RxSwiftCommunity/RxGesture.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.4; + }; + }; + 37AEB1EA2B6CEE0E00A6CDD6 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.10.2; + }; + }; + 37C161272B6BDECF000E0B21 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.8.1; + }; + }; + 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kakao/kakao-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.20.0; + }; + }; + 37C1615B2B6BEDED000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kakao/kakao-ios-sdk-rx"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.20.0; + }; + }; 3B8570C92B582B4E000BC503 /* XCRemoteSwiftPackageReference "FSCalendar" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/WenchaoD/FSCalendar.git"; @@ -1113,6 +1453,106 @@ package = 376A4B5A2B5506D7004FBB56 /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; + 37A1E9852B6C821A0013FFD7 /* RxGesture */ = { + isa = XCSwiftPackageProductDependency; + package = 37A1E9842B6C821A0013FFD7 /* XCRemoteSwiftPackageReference "RxGesture" */; + productName = RxGesture; + }; + 37AEB1EB2B6CEE0E00A6CDD6 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 37AEB1EA2B6CEE0E00A6CDD6 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 37C161282B6BDECF000E0B21 /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 37C161272B6BDECF000E0B21 /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; + 37C1613F2B6BE77A000E0B21 /* KakaoSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDK; + }; + 37C161412B6BE77A000E0B21 /* KakaoSDKAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKAuth; + }; + 37C161432B6BE77A000E0B21 /* KakaoSDKCert */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKCert; + }; + 37C161452B6BE77A000E0B21 /* KakaoSDKCertCore */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKCertCore; + }; + 37C161472B6BE77A000E0B21 /* KakaoSDKCommon */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKCommon; + }; + 37C161492B6BE77A000E0B21 /* KakaoSDKFriend */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKFriend; + }; + 37C1614B2B6BE77A000E0B21 /* KakaoSDKFriendCore */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKFriendCore; + }; + 37C1614D2B6BE77A000E0B21 /* KakaoSDKNavi */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKNavi; + }; + 37C1614F2B6BE77A000E0B21 /* KakaoSDKShare */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKShare; + }; + 37C161512B6BE77A000E0B21 /* KakaoSDKTalk */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKTalk; + }; + 37C161532B6BE77A000E0B21 /* KakaoSDKTemplate */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKTemplate; + }; + 37C161552B6BE77A000E0B21 /* KakaoSDKUser */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1613E2B6BE779000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKUser; + }; + 37C1615C2B6BEDED000E0B21 /* RxKakaoSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1615B2B6BEDED000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; + productName = RxKakaoSDK; + }; + 37C1615E2B6BEDED000E0B21 /* RxKakaoSDKAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1615B2B6BEDED000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; + productName = RxKakaoSDKAuth; + }; + 37C161602B6BEDED000E0B21 /* RxKakaoSDKCommon */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1615B2B6BEDED000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; + productName = RxKakaoSDKCommon; + }; + 37C161622B6BEDED000E0B21 /* RxKakaoSDKFriend */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1615B2B6BEDED000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; + productName = RxKakaoSDKFriend; + }; + 37C161642B6BEDED000E0B21 /* RxKakaoSDKShare */ = { + isa = XCSwiftPackageProductDependency; + package = 37C1615B2B6BEDED000E0B21 /* XCRemoteSwiftPackageReference "kakao-ios-sdk-rx" */; + productName = RxKakaoSDKShare; + }; 3B8570CA2B582B4E000BC503 /* FSCalendar */ = { isa = XCSwiftPackageProductDependency; package = 3B8570C92B582B4E000BC503 /* XCRemoteSwiftPackageReference "FSCalendar" */; diff --git a/On_off_iOS/On_off_iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/On_off_iOS/On_off_iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index e78159c..0000000 --- a/On_off_iOS/On_off_iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,50 +0,0 @@ -{ - "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", - "version" : "5.8.1" - } - }, - { - "identity" : "fscalendar", - "kind" : "remoteSourceControl", - "location" : "https://github.com/WenchaoD/FSCalendar.git", - "state" : { - "revision" : "0fbdec5172fccb90f707472eeaea4ffe095278f6", - "version" : "2.8.4" - } - }, - { - "identity" : "lottie-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-ios.git", - "state" : { - "revision" : "3f1202f254ad5d71f6c5b518ed2d6b9e2abe6969", - "version" : "4.3.4" - } - }, - { - "identity" : "rxswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ReactiveX/RxSwift.git", - "state" : { - "revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4", - "version" : "6.6.0" - } - }, - { - "identity" : "snapkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SnapKit/SnapKit.git", - "state" : { - "revision" : "e74fe2a978d1216c3602b129447c7301573cc2d8", - "version" : "5.7.0" - } - } - ], - "version" : 2 -} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/apple_login.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/apple_login.imageset/Contents.json new file mode 100644 index 0000000..31e6d5e --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/apple_login.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "appleid_button@2x (2).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/apple_login.imageset/appleid_button@2x (2).png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/apple_login.imageset/appleid_button@2x (2).png new file mode 100644 index 0000000..c46b862 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/apple_login.imageset/appleid_button@2x (2).png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/kakao_login.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/kakao_login.imageset/Contents.json new file mode 100644 index 0000000..045acdb --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/kakao_login.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "kakao_login_large_wide.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/kakao_login.imageset/kakao_login_large_wide.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/kakao_login.imageset/kakao_login_large_wide.png new file mode 100644 index 0000000..c0c1856 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/LoginDesign/kakao_login.imageset/kakao_login_large_wide.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/MemoirsCompleteImage.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/MemoirsCompleteImage.imageset/Contents.json new file mode 100644 index 0000000..373b019 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/MemoirsCompleteImage.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/MemoirsCompleteImage.imageset/Image.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/MemoirsCompleteImage.imageset/Image.png new file mode 100644 index 0000000..d51c5c5 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/MemoirsCompleteImage.imageset/Image.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/StartToWriteImage.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/StartToWriteImage.imageset/Contents.json new file mode 100644 index 0000000..bbc79ed --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/StartToWriteImage.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "StartToWriteImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/StartToWriteImage.imageset/StartToWriteImage@2x.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/StartToWriteImage.imageset/StartToWriteImage@2x.png new file mode 100644 index 0000000..ca494cd Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/StartToWriteImage.imageset/StartToWriteImage@2x.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/TextpageImage4.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/TextpageImage4.imageset/Contents.json new file mode 100644 index 0000000..251d43e --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/TextpageImage4.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "TextpageImage4@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/TextpageImage4.imageset/TextpageImage4@2x.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/TextpageImage4.imageset/TextpageImage4@2x.png new file mode 100644 index 0000000..08bd789 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/TextpageImage4.imageset/TextpageImage4@2x.png differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/\355\232\214\352\263\240\353\241\235\352\270\200\353\263\264\352\270\260\354\235\264\353\257\270\354\247\200.imageset/Contents.json" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/\355\232\214\352\263\240\353\241\235\352\270\200\353\263\264\352\270\260\354\235\264\353\257\270\354\247\200.imageset/Contents.json" new file mode 100644 index 0000000..373b019 --- /dev/null +++ "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/\355\232\214\352\263\240\353\241\235\352\270\200\353\263\264\352\270\260\354\235\264\353\257\270\354\247\200.imageset/Contents.json" @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/\355\232\214\352\263\240\353\241\235\352\270\200\353\263\264\352\270\260\354\235\264\353\257\270\354\247\200.imageset/Image.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/\355\232\214\352\263\240\353\241\235\352\270\200\353\263\264\352\270\260\354\235\264\353\257\270\354\247\200.imageset/Image.png" new file mode 100644 index 0000000..e0f5893 Binary files /dev/null and "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Memoirs/\355\232\214\352\263\240\353\241\235\352\270\200\353\263\264\352\270\260\354\235\264\353\257\270\354\247\200.imageset/Image.png" differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage1.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage1.imageset/Contents.json new file mode 100644 index 0000000..2e1c8b8 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage1.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "textpageImage1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage1.imageset/textpageImage1@2x.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage1.imageset/textpageImage1@2x.png new file mode 100644 index 0000000..46a0c0a Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage1.imageset/textpageImage1@2x.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage2.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage2.imageset/Contents.json new file mode 100644 index 0000000..64ba983 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage2.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "textpageImage2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage2.imageset/textpageImage2@2x.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage2.imageset/textpageImage2@2x.png new file mode 100644 index 0000000..7c2c61e Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage2.imageset/textpageImage2@2x.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage3.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage3.imageset/Contents.json new file mode 100644 index 0000000..d1e81a0 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage3.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "textpageImage3@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage3.imageset/textpageImage3@2x.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage3.imageset/textpageImage3@2x.png new file mode 100644 index 0000000..b080133 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/TextpageImage3.imageset/textpageImage3@2x.png differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Contents.json" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Contents.json" index 2612f39..4a25195 100644 --- "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Contents.json" +++ "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Contents.json" @@ -5,7 +5,7 @@ "scale" : "1x" }, { - "filename" : "Image 1@2x.png", + "filename" : "온보딩1@2x.png", "idiom" : "universal", "scale" : "2x" }, diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Image 1@2x.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Image 1@2x.png" deleted file mode 100644 index c1fe27e..0000000 Binary files "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/Image 1@2x.png" and /dev/null differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/\354\230\250\353\263\264\353\224\2511@2x.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/\354\230\250\353\263\264\353\224\2511@2x.png" new file mode 100644 index 0000000..a2502d5 Binary files /dev/null and "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2511.imageset/\354\230\250\353\263\264\353\224\2511@2x.png" differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Contents.json" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Contents.json" index 36fcbeb..a5e2dc5 100644 --- "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Contents.json" +++ "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Contents.json" @@ -5,7 +5,7 @@ "scale" : "1x" }, { - "filename" : "Image@2x.png", + "filename" : "온보딩2@2x.png", "idiom" : "universal", "scale" : "2x" }, diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Image@2x.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Image@2x.png" deleted file mode 100644 index dc598c1..0000000 Binary files "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/Image@2x.png" and /dev/null differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/\354\230\250\353\263\264\353\224\2512@2x.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/\354\230\250\353\263\264\353\224\2512@2x.png" new file mode 100644 index 0000000..8be3402 Binary files /dev/null and "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2512.imageset/\354\230\250\353\263\264\353\224\2512@2x.png" differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Contents.json" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Contents.json" index 373b019..bc9a73c 100644 --- "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Contents.json" +++ "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Contents.json" @@ -5,7 +5,7 @@ "scale" : "1x" }, { - "filename" : "Image.png", + "filename" : "온보딩3@2x.png", "idiom" : "universal", "scale" : "2x" }, diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Image.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Image.png" deleted file mode 100644 index 5f61df7..0000000 Binary files "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/Image.png" and /dev/null differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/\354\230\250\353\263\264\353\224\2513@2x.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/\354\230\250\353\263\264\353\224\2513@2x.png" new file mode 100644 index 0000000..3fec0bb Binary files /dev/null and "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\2513.imageset/\354\230\250\353\263\264\353\224\2513@2x.png" differ diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/Contents.json" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/Contents.json" index 373b019..22b5970 100644 --- "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/Contents.json" +++ "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/Contents.json" @@ -5,7 +5,7 @@ "scale" : "1x" }, { - "filename" : "Image.png", + "filename" : "온보딩시계@2x.png", "idiom" : "universal", "scale" : "2x" }, diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/Image.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204@2x.png" similarity index 100% rename from "On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/Image.png" rename to "On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204.imageset/\354\230\250\353\263\264\353\224\251\354\213\234\352\263\204@2x.png" diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\354\236\221.imageset/Contents.json" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\354\236\221.imageset/Contents.json" new file mode 100644 index 0000000..36fcbeb --- /dev/null +++ "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\354\236\221.imageset/Contents.json" @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git "a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\354\236\221.imageset/Image@2x.png" "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\354\236\221.imageset/Image@2x.png" new file mode 100644 index 0000000..90b7ded Binary files /dev/null and "b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/Onboarding/\354\230\250\353\263\264\353\224\251\354\213\234\354\236\221.imageset/Image@2x.png" differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl1.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl1.imageset/Contents.json new file mode 100644 index 0000000..373b019 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl1.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl1.imageset/Image.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl1.imageset/Image.png new file mode 100644 index 0000000..62e502c Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl1.imageset/Image.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl2.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl2.imageset/Contents.json new file mode 100644 index 0000000..373b019 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl2.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl2.imageset/Image.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl2.imageset/Image.png new file mode 100644 index 0000000..189bbf9 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl2.imageset/Image.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl3.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl3.imageset/Contents.json new file mode 100644 index 0000000..373b019 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl3.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl3.imageset/Image.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl3.imageset/Image.png new file mode 100644 index 0000000..762b581 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl3.imageset/Image.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl4.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl4.imageset/Contents.json new file mode 100644 index 0000000..373b019 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl4.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Image.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl4.imageset/Image.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl4.imageset/Image.png new file mode 100644 index 0000000..8b8dd54 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl4.imageset/Image.png differ diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl5.imageset/Contents.json b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl5.imageset/Contents.json new file mode 100644 index 0000000..5dbe4cd --- /dev/null +++ b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl5.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "PageControl5@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl5.imageset/PageControl5@2x.png b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl5.imageset/PageControl5@2x.png new file mode 100644 index 0000000..6ce99c5 Binary files /dev/null and b/On_off_iOS/On_off_iOS/Assets/Assets.xcassets/pageControl/PageControl5.imageset/PageControl5@2x.png differ diff --git a/On_off_iOS/On_off_iOS/Bookmark/BookmarkService.swift b/On_off_iOS/On_off_iOS/Bookmark/BookmarkService.swift new file mode 100644 index 0000000..12c0613 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Bookmark/BookmarkService.swift @@ -0,0 +1,8 @@ +// +// BookmarkService.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/03. +// + +import Foundation diff --git a/On_off_iOS/On_off_iOS/Bookmark/BookmarkTableViewCell.swift b/On_off_iOS/On_off_iOS/Bookmark/BookmarkTableViewCell.swift new file mode 100644 index 0000000..77060d4 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Bookmark/BookmarkTableViewCell.swift @@ -0,0 +1,74 @@ +// +// BookmarkTableViewCell.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/03. +// + +import UIKit +import SnapKit + +final class BookmarkTableViewCell: UITableViewCell { + + private lazy var iconImageView = UIImageView() + + private let dateLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 22, weight: .bold) + return label + }() + + // MARK: - Init + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + addSubview(iconImageView) + addSubview(dateLabel) + + iconImageView.contentMode = .scaleAspectFit + dateLabel.textAlignment = .center + + iconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.height.equalToSuperview().multipliedBy(0.8) + make.width.equalTo(iconImageView.snp.height) + } + + dateLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + func configure(with item: Item, at indexPath: IndexPath) { + dateLabel.text = item.title + iconImageView.image = item.image + iconImageView.backgroundColor = .blue + + iconImageView.snp.remakeConstraints { make in + make.centerY.equalToSuperview() + make.height.equalToSuperview().multipliedBy(0.8) + make.width.equalTo(iconImageView.snp.height) + + if indexPath.row % 2 == 0 { + make.leading.equalToSuperview().offset(10) + } else { + make.trailing.equalToSuperview().offset(-10) + } + } + } +} + +/// 더미 데이터 예시 형식임 ❎ +struct Item { + var title: String + var image : UIImage +} diff --git a/On_off_iOS/On_off_iOS/Bookmark/BookmarkViewController.swift b/On_off_iOS/On_off_iOS/Bookmark/BookmarkViewController.swift new file mode 100644 index 0000000..f0ed9e1 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Bookmark/BookmarkViewController.swift @@ -0,0 +1,100 @@ +// +// BookmarkViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/03. +// + +import UIKit +import RxSwift +import RxCocoa +import SnapKit + +final class BookmarkViewController: UIViewController { + + private lazy var tableView = UITableView() + // 더미 데이터❎ + var items = PublishSubject<[Item]>() + + private let viewModel: BookmarkViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: BookmarkViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + bindTableView() + setupBindings() + } + + private func setupTableView() { + view.addSubview(tableView) + tableView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + tableView.register(BookmarkTableViewCell.self, forCellReuseIdentifier: CellIdentifier.BookmarkTableViewCell.rawValue) + tableView.rx.setDelegate(self).disposed(by: disposeBag) + } + + private func bindTableView() { + items + .bind(to: tableView.rx.items(cellIdentifier: CellIdentifier.BookmarkTableViewCell.rawValue, cellType: BookmarkTableViewCell.self)) { (row, item, cell) in + cell.configure(with: item, at: IndexPath(row: row, section: 0)) + } + .disposed(by: disposeBag) + + // 더미 데이터 생성, 바인딩 + let dummyItems = (1...20).map { Item(title: "Item \($0)", image: UIImage(named: "AppIcon") ?? UIImage()) } + items.onNext(dummyItems) + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + + let cellTapped = tableView.rx.itemSelected + .map { [unowned self] indexPath -> Item in + try! self.tableView.rx.model(at: indexPath) + } + .share() + + cellTapped + .subscribe(onNext: { item in + print("Selected item: \(item.title)") + }) + .disposed(by: disposeBag) + + // ViewModel의 Input 생성 및 바인딩 + let input = BookmarkViewModel.Input(cellTapped: cellTapped.map { $0.title }) + viewModel.bind(input: input) + + tableView.rx.modelSelected(Item.self) + .subscribe(onNext: { [weak self] item in + self?.navigateToDetail(for: item) + }) + .disposed(by: disposeBag) + + } + + private func navigateToDetail(for item: Item) { + let memoirsViewModel = MemoirsViewModel() + let memoirsViewController = MemoirsViewController(viewModel: memoirsViewModel) + self.navigationController?.pushViewController(memoirsViewController, animated: true) + } +} + +/// UITableViewDelegate +extension BookmarkViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UIScreen.main.bounds.height * 0.2 + } +} diff --git a/On_off_iOS/On_off_iOS/Bookmark/BookmarkViewModel.swift b/On_off_iOS/On_off_iOS/Bookmark/BookmarkViewModel.swift new file mode 100644 index 0000000..b79fb0e --- /dev/null +++ b/On_off_iOS/On_off_iOS/Bookmark/BookmarkViewModel.swift @@ -0,0 +1,38 @@ +// +// BookmarkViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/03. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// BookmarkViewModel +final class BookmarkViewModel { + + private let disposeBag = DisposeBag() + + /// Input + struct Input { + let cellTapped: Observable + } + + /// Output + struct Output { + } + + /// binding Input + /// - Parameters: + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) { + input.cellTapped + .subscribe(onNext: { [weak self] title in + print("선택된거 날짜 보여줄꺼임: \(title)") + }) + .disposed(by: disposeBag) + } +} diff --git a/On_off_iOS/On_off_iOS/Bookmark/KeyChain/KeyChain.swift b/On_off_iOS/On_off_iOS/Bookmark/KeyChain/KeyChain.swift new file mode 100644 index 0000000..588452a --- /dev/null +++ b/On_off_iOS/On_off_iOS/Bookmark/KeyChain/KeyChain.swift @@ -0,0 +1,52 @@ +// +// KeyChain.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/04. +// + +import Foundation + +// MARK: - ❎키체인어캐 나눌지 말해보기 +/// MemoirsKeyChain : 키체인 +enum MemoirsKeyChain: String { + case MemoirsAnswer1 + case MemoirsAnswer2 + case MemoirsAnswer3 + case emoticonID +} + +/// LoginKeyChain : 키체인 +enum LoginKeyChain: String { + case accessToken + case refreshToken +} + +/// login 종류 식별 +enum LoginMethod: String{ + case loginMethod +} + +/// 카카오 로그인 키체인값 +enum KakaoLoginKeyChain: String { + case accessToken + case idToken +} + +/// 애플 로그인 키체인값 +enum AppleLoginKeyChain: String { + case oauthId + case giveName + case familyName + case email + case identityTokenString + case authorizationCodeString +} + +/// 프로필 키체인 값 +enum ProfileKeyChain: String { + case nickname + case fieldOfWork + case job + case experienceYear +} diff --git a/On_off_iOS/On_off_iOS/Constants/API/Domain.swift b/On_off_iOS/On_off_iOS/Constants/API/Domain.swift index c232041..a4f654a 100644 --- a/On_off_iOS/On_off_iOS/Constants/API/Domain.swift +++ b/On_off_iOS/On_off_iOS/Constants/API/Domain.swift @@ -2,7 +2,7 @@ // Domain.swift // On_off_iOS // -// Created by 신예진 on 2/13/24. +// Created by 박다미 on 2024/01/31. // import Foundation diff --git a/On_off_iOS/On_off_iOS/Constants/API/EndPoint.swift b/On_off_iOS/On_off_iOS/Constants/API/EndPoint.swift index f68047e..5d1b743 100644 --- a/On_off_iOS/On_off_iOS/Constants/API/EndPoint.swift +++ b/On_off_iOS/On_off_iOS/Constants/API/EndPoint.swift @@ -25,19 +25,3 @@ enum MemoirsPath: String { case memoirsSave = "/Memoirs" case getEmoticon = "/emoticons" } - -enum FeedPath: String { - case feedImage = "/feed-images" - case workLifeBalacne = "/feeds?date=DATE" - case checkWLB = "/feeds/FEEDID/check" - case delayTomorrow = "/feeds/FEEDID/delay" - case delete = "/feeds/FEEDID" -} - -///ON-업무일지 -enum WorklogPath: String { - case worklog = "/on/worklog/worklogid" -// case checkWLP = "/on/worklog/worklogid" -// case delaylogTommorow = "/on/worklog/worklogId" -// case deletelog = "/on/worklog/worklogId" -} diff --git a/On_off_iOS/On_off_iOS/Constants/Header.swift b/On_off_iOS/On_off_iOS/Constants/Header.swift index 647a79f..c2496ae 100644 --- a/On_off_iOS/On_off_iOS/Constants/Header.swift +++ b/On_off_iOS/On_off_iOS/Constants/Header.swift @@ -2,7 +2,7 @@ // Header.swift // On_off_iOS // -// Created by 신예진 on 2/13/24. +// Created by 박다미 on 2024/02/01. // import Alamofire diff --git a/On_off_iOS/On_off_iOS/Extension/Ext + NSAttributedString.swift b/On_off_iOS/On_off_iOS/Extension/Ext + NSAttributedString.swift new file mode 100644 index 0000000..6c2900f --- /dev/null +++ b/On_off_iOS/On_off_iOS/Extension/Ext + NSAttributedString.swift @@ -0,0 +1,38 @@ +// +// Ext + NSAttributedString.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/06. +// + +import UIKit + +/// text중 포인드로 줄 단어만 다른 색으로 바꾸기 +extension NSAttributedString { + static func createAttributedText(mainText: String, highlightTexts: [String], attributes: [NSAttributedString.Key: Any]) -> NSAttributedString { + let attributedString = NSMutableAttributedString(string: mainText) + + highlightTexts.forEach { text in + let range = (mainText as NSString).range(of: text) + if range.location != NSNotFound { + attributedString.addAttributes(attributes, range: range) + } + } + + return attributedString + } +} + +extension OnBoardingViewController { + func createAttributedText(for text: String, highlightWords: [(String, UIColor, UIFont)]) -> NSAttributedString { + let attributedString = NSMutableAttributedString(string: text) + highlightWords.forEach { word, color, font in + let range = (text as NSString).range(of: word) + if range.location != NSNotFound { + attributedString.addAttributes([.foregroundColor: color, .font: font], range: range) + } + } + return attributedString + } +} + diff --git a/On_off_iOS/On_off_iOS/Extension/Ext + UIImage.swift b/On_off_iOS/On_off_iOS/Extension/Ext + UIImage.swift index 2662396..692b435 100644 --- a/On_off_iOS/On_off_iOS/Extension/Ext + UIImage.swift +++ b/On_off_iOS/On_off_iOS/Extension/Ext + UIImage.swift @@ -32,33 +32,58 @@ extension UIImage { } return renderImage } - - /// MARK: 이미지 회전하는 함수 - func rotate(degrees: CGFloat) -> UIImage { - /// context에 그려질 크기를 구하기 위해서 최종 회전되었을때의 전체 크기 획득 - let rotatedViewBox: UIView = UIView(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height)) - let affineTransform: CGAffineTransform = CGAffineTransform(rotationAngle: degrees * CGFloat.pi / 180) - rotatedViewBox.transform = affineTransform + +// /// MARK: 이미지 회전하는 함수 +// func rotate(degrees: CGFloat) -> UIImage { +// /// context에 그려질 크기를 구하기 위해서 최종 회전되었을때의 전체 크기 획득 +// let rotatedViewBox: UIView = UIView(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height)) +// let affineTransform: CGAffineTransform = CGAffineTransform(rotationAngle: degrees * CGFloat.pi / 180) +// rotatedViewBox.transform = affineTransform +// +// /// 회전된 크기 +// let rotatedSize: CGSize = rotatedViewBox.frame.size +// +// /// 회전한 만큼의 크기가 있을때, 필요없는 여백 부분을 제거하는 작업 +// UIGraphicsBeginImageContext(rotatedSize) +// let bitmap: CGContext = UIGraphicsGetCurrentContext()! +// /// 원점을 이미지의 가운데로 평행 이동 +// bitmap.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2) +// /// 회전 +// bitmap.rotate(by: (degrees * CGFloat.pi / 180)) +// /// 상하 대칭 변환 후 context에 원본 이미지 그림 그리는 작업 +// bitmap.scaleBy(x: 1.0, y: -1.0) +// bitmap.draw(cgImage!, in: CGRect(x: -size.width / 2, y: -size.height / 2, width: size.width, height: size.height)) +// +// /// 그려진 context로 부터 이미지 획득 +// guard let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() } +// UIGraphicsEndImageContext() +// +// return newImage +// } + + /// 이미지 90도 회전 + /// - Parameter radians: radians + /// - Returns: rotatedImage + func rotated(by radians: CGFloat) -> UIImage? { + guard let cgImage = self.cgImage else { return nil } - /// 회전된 크기 - let rotatedSize: CGSize = rotatedViewBox.frame.size + let rotatedSize = CGRect(origin: .zero, size: size) + .applying(CGAffineTransform(rotationAngle: radians)) + .integral.size - /// 회전한 만큼의 크기가 있을때, 필요없는 여백 부분을 제거하는 작업 - UIGraphicsBeginImageContext(rotatedSize) - let bitmap: CGContext = UIGraphicsGetCurrentContext()! - /// 원점을 이미지의 가운데로 평행 이동 - bitmap.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2) - /// 회전 - bitmap.rotate(by: (degrees * CGFloat.pi / 180)) - /// 상하 대칭 변환 후 context에 원본 이미지 그림 그리는 작업 - bitmap.scaleBy(x: 1.0, y: -1.0) - bitmap.draw(cgImage!, in: CGRect(x: -size.width / 2, y: -size.height / 2, width: size.width, height: size.height)) + UIGraphicsBeginImageContextWithOptions(rotatedSize, false, scale) + guard let context = UIGraphicsGetCurrentContext() else { return nil } - /// 그려진 context로 부터 이미지 획득 - guard let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() } + /// 기준점을 이미지 중앙으로 이동 + context.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2) + /// 주어진 각도만큼 회전 + context.rotate(by: radians) + /// 이미지를 새 위치에 그리기 + context.draw(cgImage, in: CGRect(x: -size.width / 2, y: -size.height / 2, width: size.width, height: size.height)) + + let rotatedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return newImage + return rotatedImage } - } diff --git a/On_off_iOS/On_off_iOS/GoogleService-Info.plist b/On_off_iOS/On_off_iOS/GoogleService-Info.plist new file mode 100644 index 0000000..4eec2f4 --- /dev/null +++ b/On_off_iOS/On_off_iOS/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyDTtNHV1RmTLG2HeH_MBfaSbmH8kkLCjNw + GCM_SENDER_ID + 954980651097 + PLIST_VERSION + 1 + BUNDLE_ID + UMC-OnAndOff.On-off-iOS + PROJECT_ID + onandoff-49ed9 + STORAGE_BUCKET + onandoff-49ed9.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:954980651097:ios:ce19c40ffaf8f1b8a9e148 + + \ No newline at end of file diff --git a/On_off_iOS/On_off_iOS/Info.plist b/On_off_iOS/On_off_iOS/Info.plist index 54a3c5e..e748c01 100644 --- a/On_off_iOS/On_off_iOS/Info.plist +++ b/On_off_iOS/On_off_iOS/Info.plist @@ -2,6 +2,28 @@ + PORT + ${PORT} + IP + ${IP} + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + KAKAO_NATIVE_APP_KEY + ${KAKAO_NATIVE_APP_KEY} + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + kakao${KAKAO_NATIVE_APP_KEY} + + + UIAppFonts Pretendard-Black.otf @@ -31,5 +53,11 @@ + LSApplicationQueriesSchemes + + kakaokompassauth + kakaolink + kakaoplus + diff --git a/On_off_iOS/On_off_iOS/Launch/LaunchModel.swift b/On_off_iOS/On_off_iOS/Launch/LaunchModel.swift new file mode 100644 index 0000000..27ef1ea --- /dev/null +++ b/On_off_iOS/On_off_iOS/Launch/LaunchModel.swift @@ -0,0 +1,14 @@ +// +// LaunchModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/10. +// + +import Foundation + +enum LaunchNavigation { + case main + case onBoarding + case login +} diff --git a/On_off_iOS/On_off_iOS/Launch/LaunchViewController.swift b/On_off_iOS/On_off_iOS/Launch/LaunchViewController.swift index 77b429e..c460c4c 100644 --- a/On_off_iOS/On_off_iOS/Launch/LaunchViewController.swift +++ b/On_off_iOS/On_off_iOS/Launch/LaunchViewController.swift @@ -5,10 +5,10 @@ // Created by 박다미 on 2024/01/01. // -import Foundation -import UIKit import Lottie import SnapKit +import RxSwift +import UIKit /// LaunchViewController - Launch화면 final class LaunchViewController: UIViewController { @@ -20,38 +20,78 @@ final class LaunchViewController: UIViewController { view.loopMode = .playOnce return view }() - + + private var viewModel: LaunchViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: LaunchViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - ViewDidLoad override func viewDidLoad() { - super.viewDidLoad() - + super.viewDidLoad() + // KeychainWrapper.delete(key: LoginKeyChain.refreshToken.rawValue) + setupAnim() - setupPassTime() - } + finishAnimation() + } - /// mark: -로티 화면 세팅 - private func setupAnim(){ + /// Setup Lottie animation + private func setupAnim() { view.addSubview(animationView) animationView.snp.makeConstraints { make in make.edges.equalToSuperview() } } - /// 애니메이션 끝나는 시간 - private func setupPassTime(){ - animationView.play { [weak self] (finished) in - guard let self = self else { return } - if finished { - moveToMain() + /// Setup animation 시간 완료 동작 + private func finishAnimation() { + animationView.play { [weak self] finished in + if finished { + self?.navigateAfterAnimation() + } + } + } + + private func navigateAfterAnimation() { + let input = LaunchViewModel.Input(animationCompleted: Observable.just(())) + let output = viewModel.bind(input: input) + output.navigationSignal + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] navigation in + self?.navigate(to: navigation) + }) + .disposed(by: disposeBag) + } + /// 화면 전환 로직 + private func navigate(to navigation: LaunchNavigation) { + DispatchQueue.main.async { [weak self] in + guard let self = self, let navigationController = self.navigationController else { return } + + let viewController: UIViewController + switch navigation { + case .main: + let viewModel = BookmarkViewModel() + viewController = BookmarkViewController(viewModel: viewModel) + + case .onBoarding: + let viewModel = OnBoardingViewModel() + viewController = OnBoardingViewController(viewModel: viewModel) + + case .login: + let viewModel = LoginViewModel(loginService: LoginService()) + viewController = LoginViewController(viewModel: viewModel) } + + navigationController.setViewControllers([viewController], animated: true) } } - - /// 화면 이동 - private func moveToMain() { - if let navigationController = self.navigationController { - let onBoardingViewController = OnBoardingViewController(viewModel: OnBoardingViewModel(navigationController: navigationController)) - navigationController.pushViewController(onBoardingViewController, animated: true) - } - } } diff --git a/On_off_iOS/On_off_iOS/Launch/LaunchViewModel.swift b/On_off_iOS/On_off_iOS/Launch/LaunchViewModel.swift new file mode 100644 index 0000000..fb01d47 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Launch/LaunchViewModel.swift @@ -0,0 +1,57 @@ +// +// LaunchViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/09. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// LaunchViewModel +final class LaunchViewModel { + + private let loginService: LoginService + private let disposeBag = DisposeBag() + + struct Input { + /// 애니메이션 완료 신호를 처리할 Relay + let animationCompleted: Observable + } + + struct Output { + /// 네비게이션 방향을 알리는 Observable + let navigationSignal: Observable + + } + + // MARK: - Init + init(loginService: LoginService) { + self.loginService = loginService + } + + func bind(input: Input) -> Output { + let navigationSignal = input.animationCompleted + .flatMapLatest { [weak self] _ -> Observable in + guard let self = self, + let refreshToken = KeychainWrapper.loadItem(forKey: LoginKeyChain.refreshToken.rawValue), + let accessToken = KeychainWrapper.loadItem(forKey: LoginKeyChain.accessToken.rawValue) else { + return .just(.onBoarding) // 키체인에 정보가 없으면 온보딩으로 + } + + // 토큰 유효성 검사 요청 + let request = TokenResult(accessToken: accessToken, refreshToken: refreshToken) + return self.loginService.validateTokenAndSendInfo(request: request) + .map { response in + response.isSuccess ? .main : .login // 응답 성공 시 메인, 실패 시 로그인으로 + } + .catchAndReturn(.login) // 오류 발생 시 로그인 화면으로 + } + + return Output(navigationSignal: navigationSignal) + + } +} + diff --git a/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileTableViewCell.swift b/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileTableViewCell.swift new file mode 100644 index 0000000..581d67e --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileTableViewCell.swift @@ -0,0 +1,42 @@ +// +// ModalSelectProfileTableViewCell.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/05. +// + +import UIKit +import SnapKit + +final class ModalSelectProfileTableViewCell: UITableViewCell { + + private let label: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.textAlignment = .left + label.font = UIFont.systemFont(ofSize: 15, weight: .regular) + return label + }() + + // MARK: - Init + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Setup Views + private func setupViews() { + contentView.addSubview(label) + label.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(10) + } + } + + func configure(with text: String) { + self.label.text = text + } +} diff --git a/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileViewController.swift b/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileViewController.swift new file mode 100644 index 0000000..17821c6 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileViewController.swift @@ -0,0 +1,97 @@ +// +// ModalSelectProfileViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/05. +// + +import RxSwift +import RxCocoa +import SnapKit +import UIKit + +final class ModalSelectProfileViewController: UIViewController { + + private lazy var label: UILabel = { + let label = UILabel() + label.textColor = .OnOffMain + label.font = .systemFont(ofSize: 20, weight: .bold) + return label + }() + + private lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .white + tableView.register(ModalSelectProfileTableViewCell.self, forCellReuseIdentifier: CellIdentifier.ModalSelectProfileTableViewCell.rawValue) + tableView.separatorStyle = .none + return tableView + }() + + private let viewModel: ModalSelectProfileViewModel + private let disposeBag = DisposeBag() + private var dataType: ProfileDataType + + var onImageSelected: ((String) -> Void)? + + weak var delegate: ModalSelectProfileDelegate? + + // MARK: - Init + init(viewModel: ModalSelectProfileViewModel, dataType: ProfileDataType) { + self.viewModel = viewModel + self.dataType = dataType + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - viewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + setupConstraints() + setupBindings() + } + + /// setupViews + private func setupViews() { + view.backgroundColor = .white + view.addSubview(label) + view.addSubview(tableView) + } + + private func setupConstraints() { + + label.snp.makeConstraints { make in + make.top.leading.equalToSuperview().inset(30) + } + tableView.snp.makeConstraints { make in + make.top.equalTo(label.snp.bottom).offset(20) + make.leading.trailing.bottom.equalToSuperview().inset(20) + } + } + + private func setupBindings() { + let input = ModalSelectProfileViewModel.Input(viewDidLoad: Observable.just(())) + let output = viewModel.bind(input: input, dataType: dataType) + + output.options + .bind(to: tableView.rx.items(cellIdentifier: CellIdentifier.ModalSelectProfileTableViewCell.rawValue, cellType: ModalSelectProfileTableViewCell.self)) { (row, element, cell) in + cell.configure(with: element) + } + .disposed(by: disposeBag) + + output.labelText + .bind(to: label.rx.text) + .disposed(by: disposeBag) + + tableView.rx.modelSelected(String.self) + .bind { [weak self] selectedOption in + self?.delegate?.optionSelected(data: selectedOption, dataType: self?.dataType ?? .fieldOfWork) + self?.dismiss(animated: true, completion: nil) + } + .disposed(by: disposeBag) + } +} diff --git a/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileViewModel.swift b/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileViewModel.swift new file mode 100644 index 0000000..4cbcd4a --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Modal/ModalSelectProfileViewModel.swift @@ -0,0 +1,51 @@ +// +// ModalSelectProfileViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/05. +// + +import RxCocoa +import RxSwift + +final class ModalSelectProfileViewModel { + + private let disposeBag = DisposeBag() + private let loginService = LoginService() + + /// Input + struct Input { + + /// 뷰가 로드될 때 이벤트 처리 + let viewDidLoad: Observable + // let dataSelected: Observable + + } + + /// Output + struct Output { + let options: Observable<[String]> + let labelText: Observable + } + + /// bind + /// - Parameter input: input + /// - Returns: output + /// 입력을 받아 처리하고 출력을 반환 + func bind(input: Input, dataType: ProfileDataType) -> Output { + let options = input.viewDidLoad + .flatMapLatest { [weak self] _ -> Observable<[String]> in + guard let self = self else { return .empty() } + switch dataType { + case .fieldOfWork: + return self.loginService.fetchJobOptions().map { $0.result }.catchAndReturn([]) + + case .experienceYear: + return self.loginService.fetchExperienceYearsOptions().map { $0.result }.catchAndReturn([]) + } + } + let labelText = Observable.just(dataType == .fieldOfWork ? "업무 분야" : "연차") + + return Output(options: options, labelText: labelText) + } +} diff --git a/On_off_iOS/On_off_iOS/LogIn/ModalSelectProfileDelegate.swift b/On_off_iOS/On_off_iOS/LogIn/ModalSelectProfileDelegate.swift new file mode 100644 index 0000000..64147fb --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/ModalSelectProfileDelegate.swift @@ -0,0 +1,13 @@ +// +// ModalSelectProfileDelegate.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/05. +// + +import Foundation + +/// ModalSelectProfileDelegate: 프로필 모달창에서 메인화면에 데이터 넘기기 +protocol ModalSelectProfileDelegate: AnyObject { + func optionSelected(data: String, dataType: ProfileDataType) +} diff --git a/On_off_iOS/On_off_iOS/LogIn/Model/Login.swift b/On_off_iOS/On_off_iOS/LogIn/Model/Login.swift new file mode 100644 index 0000000..3c92ba9 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Model/Login.swift @@ -0,0 +1,54 @@ +// +// Login.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/01. +// + +import Foundation + +/// 카카오 토큰 검증 요청 구조체 +struct KakaoTokenValidationRequest: Codable { + let identityToken: String + let accessToken: String + let additionalInfo: AdditionalInfo +} + +/// 애플 로그인 요청 구조체 +struct AppleTokenValidationRequest: Codable { + let oauthId: String + let fullName: FullName + let email: String + let identityToken: String + let authorizationCode: String + let additionalInfo: AdditionalInfo +} + +/// 추가 정보 구조체: 프로필: 분야, 직업, 경력 +struct AdditionalInfo: Codable { + let nickname: String + let fieldOfWork: String + let job: String + let experienceYear: String +} + +/// 사용자 이름 구조체 +struct FullName: Codable { + let giveName: String + let familyName: String +} + +/// ❎추후 Login파일말고 공동사용 구조체로서 이동할 파일 말할것 +/// 응답구조체 Response +struct Response: Codable { + let isSuccess: Bool + let code: String + let message: String + let result: T +} + + +struct TokenResult: Codable { + let accessToken: String + let refreshToken: String +} diff --git a/On_off_iOS/On_off_iOS/LogIn/Model/ProfileDataType.swift b/On_off_iOS/On_off_iOS/LogIn/Model/ProfileDataType.swift new file mode 100644 index 0000000..8d302d9 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Model/ProfileDataType.swift @@ -0,0 +1,13 @@ +// +// ProfileDataType.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/05. +// + +import Foundation + +enum ProfileDataType { + case fieldOfWork + case experienceYear +} diff --git a/On_off_iOS/On_off_iOS/NickName/View/NickNameViewController.swift b/On_off_iOS/On_off_iOS/LogIn/NickName/View/NickNameViewController.swift similarity index 71% rename from On_off_iOS/On_off_iOS/NickName/View/NickNameViewController.swift rename to On_off_iOS/On_off_iOS/LogIn/NickName/View/NickNameViewController.swift index 34eb558..cf47916 100644 --- a/On_off_iOS/On_off_iOS/NickName/View/NickNameViewController.swift +++ b/On_off_iOS/On_off_iOS/LogIn/NickName/View/NickNameViewController.swift @@ -14,19 +14,34 @@ final class NickNameViewController: UIViewController { private let welcomeLabel: UILabel = { let label = UILabel() - label.text = "일의 성장과\n삶의 밸런스를 위한\n준비를 시작해볼까요?" label.numberOfLines = 0 label.textAlignment = .left label.font = UIFont.systemFont(ofSize: 24, weight: .bold) + + label.attributedText = .createAttributedText( + mainText: "일의 성장과\n삶의 밸런스를 위한\n준비를 시작해볼까요?", + highlightTexts: ["삶의 밸런스"], + attributes: [ + .foregroundColor: UIColor.OnOffMain, + .font: UIFont.boldSystemFont(ofSize: 24) + ] + ) return label }() /// 닉네임 관련 private let nickNameLabel: UILabel = { let label = UILabel() - label.text = "닉네임을 설정해주세요" label.textColor = .black label.font = .systemFont(ofSize: 20, weight: .bold) + label.attributedText = .createAttributedText( + mainText: "닉네임을 설정해주세요", + highlightTexts: ["닉네임"], + attributes: [ + .foregroundColor: UIColor.OnOffMain, + .font: UIFont.boldSystemFont(ofSize: 20) + ] + ) return label }() @@ -98,9 +113,22 @@ final class NickNameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white + settingView() addSubviews() setupBindings() } + private func settingView(){ + view.backgroundColor = .white + + /// 버튼 테두리 설정 + setupcheckButtonView() + } + /// 버튼 테두리 설정 + private func setupcheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .longButtonCornerRadius, width: view.frame.width) + checkButtonView.layer.cornerRadius = cornerRadius + checkButtonView.layer.masksToBounds = true + } /// addSubviews private func addSubviews(){ @@ -167,18 +195,36 @@ final class NickNameViewController: UIViewController { /// 글자수 출력 바인딩 output.nickNameLength - .map { "(\($0)/10)" } + .map { "(\($0)/10)" } .bind(to: checkLenghtLabel.rx.text) .disposed(by: disposeBag) - + // 버튼 활성화 상태 및 색상 변경 바인딩 output.isCheckButtonEnabled - .observe(on: MainScheduler.instance) - .bind { [weak self] isEnabled in - self?.checkButton.isEnabled = isEnabled - self?.checkButtonView.backgroundColor = isEnabled ? UIColor.blue : UIColor.lightGray - } - .disposed(by: disposeBag) + .observe(on: MainScheduler.instance) + .bind { [weak self] isEnabled in + guard let self = self else { return } + checkButton.isEnabled = isEnabled + checkButtonView.layer.borderColor = UIColor.OnOffMain.cgColor + checkButtonView.layer.borderWidth = 1 + + checkButtonView.backgroundColor = isEnabled ? UIColor.OnOffMain : .white + checkButton.setTitleColor(isEnabled ? .white : UIColor.OnOffMain, for: .normal) + } + .disposed(by: disposeBag) + + output.moveToNext + .subscribe(onNext: { [weak self] _ in + self?.moveToProfile() + }) + .disposed(by: disposeBag) + } + + /// 프로필설정으로 이동 + private func moveToProfile() { + let profileViewModel = ProfileSettingViewModel(loginService: LoginService()) + let profileSettingViewController = ProfileSettingViewController(viewModel: profileViewModel) + self.navigationController?.pushViewController(profileSettingViewController, animated: true) } // 키보드내리기 diff --git a/On_off_iOS/On_off_iOS/NickName/ViewModel/NickNameViewModel.swift b/On_off_iOS/On_off_iOS/LogIn/NickName/ViewModel/NickNameViewModel.swift similarity index 72% rename from On_off_iOS/On_off_iOS/NickName/ViewModel/NickNameViewModel.swift rename to On_off_iOS/On_off_iOS/LogIn/NickName/ViewModel/NickNameViewModel.swift index e658f28..d2ea38c 100644 --- a/On_off_iOS/On_off_iOS/NickName/ViewModel/NickNameViewModel.swift +++ b/On_off_iOS/On_off_iOS/LogIn/NickName/ViewModel/NickNameViewModel.swift @@ -12,7 +12,6 @@ import UIKit final class NickNameViewModel { private let disposeBag = DisposeBag() - var navigationController: UINavigationController /// Input struct Input { @@ -25,11 +24,7 @@ final class NickNameViewModel { let nickNameFilteringRelay: BehaviorRelay = BehaviorRelay(value: "") let nickNameLength: PublishSubject = PublishSubject() let isCheckButtonEnabled: BehaviorRelay = BehaviorRelay(value: true) - } - - // MARK: - Init - init(navigationController: UINavigationController) { - self.navigationController = navigationController + let moveToNext = PublishSubject() } /// binding Input @@ -53,11 +48,13 @@ final class NickNameViewModel { /// 완료버튼 클릭 input.startButtonTapped - .bind { [weak self] in - self?.moveToProfile() - } - .disposed(by: disposeBag) - + .withLatestFrom(input.nickNameTextChanged) + .subscribe(onNext: { nickname in + _ = KeychainWrapper.saveItem(value: nickname, forKey: ProfileKeyChain.nickname.rawValue) + output.moveToNext.onNext(()) + }) + .disposed(by: disposeBag) + return output } @@ -66,11 +63,4 @@ final class NickNameViewModel { let regex = "^[가-힣A-Za-z0-9.,!_~]+$" return nickName.range(of: regex, options: .regularExpression) != nil } - - /// 프로필설정으로 이동 - private func moveToProfile() { - let profileViewModel = ProfileSettingViewModel(navigationController: navigationController) - let vc = ProfileSettingViewController(viewModel: profileViewModel) - navigationController.pushViewController(vc, animated: true) - } } diff --git a/On_off_iOS/On_off_iOS/LogIn/ProfileSetting/View/ProfileSettingViewController.swift b/On_off_iOS/On_off_iOS/LogIn/ProfileSetting/View/ProfileSettingViewController.swift new file mode 100644 index 0000000..10a8dd3 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/ProfileSetting/View/ProfileSettingViewController.swift @@ -0,0 +1,396 @@ +// +// ProfileSettingViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/01. +// + +import UIKit +import RxSwift +import RxCocoa + +/// 닉네임 설정 +final class ProfileSettingViewController: UIViewController { + + /// 업무분야 + private lazy var fieldOfWork: UILabel = { + let label = UILabel() + label.text = "업무 분야" + label.textColor = .black + label.font = .systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 업무분야 - 텍스트 필드 + private lazy var fieldOfWorkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("선택해 주세요", for: .normal) + button.setTitleColor(.black, for: .normal) + button.backgroundColor = .clear + button.layer.borderWidth = 0 + button.layer.borderColor = UIColor.lightGray.cgColor + button.contentHorizontalAlignment = .left + return button + }() + + private lazy var fieldOfWorkDownImage: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "chevron.down") + imageView.tintColor = .black + imageView.contentMode = .scaleAspectFit + return imageView + }() + /// 업무분야 - 밑줄 + private lazy var fieldOfWorkLine: UIView = { + let lineView = UIView() + lineView.backgroundColor = .OnOffMain + return lineView + }() + + /// 직업 + private lazy var job: UILabel = { + let label = UILabel() + label.text = "직업" + label.textColor = .black + label.font = .systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 직업 - 텍스트 필드 + private lazy var jobTextField: UITextField = { + let field = UITextField() + field.attributedPlaceholder = NSAttributedString(string: "예시) 서비스 기획자, UX 디자이너, 개발자 등", + attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray]) + field.textAlignment = .left + field.backgroundColor = UIColor.clear + field.layer.cornerRadius = 10 + field.layer.borderWidth = 1 + field.font = UIFont.systemFont(ofSize: 13, weight: .regular) + field.layer.borderColor = UIColor.clear.cgColor + field.textColor = .black + + return field + }() + + /// 직업 - 글자수 + private let checkLenghtJobLabel: UILabel = { + let label = UILabel() + label.text = "(0/30)" + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 13, weight: .regular) + label.textColor = .lightGray + return label + }() + /// 직업 - 밑줄 + private lazy var jobLine : UIView = { + let lineView = UIView() + lineView.backgroundColor = .OnOffMain + return lineView + }() + + /// 연차 + private lazy var annual: UILabel = { + let label = UILabel() + label.text = "연차" + label.textColor = .black + label.font = .systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 연차 - 버튼 + private lazy var annualButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("선택해 주세요", for: .normal) + button.setTitleColor(.black, for: .normal) + button.backgroundColor = .clear + button.layer.borderWidth = 0 + button.layer.borderColor = UIColor.lightGray.cgColor + button.contentHorizontalAlignment = .left + return button + }() + + private lazy var annualDownImage: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "chevron.down") + imageView.contentMode = .scaleAspectFit + imageView.tintColor = .black + return imageView + }() + + /// 연차 - 밑줄 + private lazy var annualLine: UIView = { + let lineView = UIView() + lineView.backgroundColor = .OnOffMain + return lineView + }() + + /// 닉네임 설명 라벨 + private let nickNameExplainLabel: UILabel = { + let label = UILabel() + label.text = " 업무 분야, 직업, 연차는 추후에 ‘마이 페이지’에서 수정할 수 있어요. " + label.numberOfLines = 1 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 10, weight: .regular) + return label + }() + + /// 확인 버튼 + private let checkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("확인", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + return button + }() + + /// 확인버튼 뷰 + private lazy var checkButtonView: UIView = { + let view = UIView() + return view + }() + + private var viewModel: ProfileSettingViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: ProfileSettingViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + + settingView() + addSubviews() + setupBindings() + } + + private func settingView(){ + view.backgroundColor = UIColor.white + setupCheckButtonView() + + } + + /// 시작 버튼 속성 설정 + private func setupCheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .longButtonCornerRadius, width: view.frame.width) + checkButtonView.layer.cornerRadius = cornerRadius + checkButtonView.layer.masksToBounds = true + } + // 키보드내리기 + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + jobTextField.endEditing(true) + } + + /// addSubviews + private func addSubviews(){ + + view.addSubview(fieldOfWork) + view.addSubview(fieldOfWorkButton) + view.addSubview(fieldOfWorkDownImage) + view.addSubview(fieldOfWorkLine) + + view.addSubview(job) + view.addSubview(jobTextField) + view.addSubview(checkLenghtJobLabel) + view.addSubview(jobLine) + + view.addSubview(annual) + view.addSubview(annualButton) + view.addSubview(annualDownImage) + view.addSubview(annualLine) + + view.addSubview(nickNameExplainLabel) + view.addSubview(checkButtonView) + checkButtonView.addSubview(checkButton) + configureConstraints() + } + + /// configureConstraints + private func configureConstraints(){ + + fieldOfWork.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide).offset(100) + make.leading.equalToSuperview().offset(10) + } + + fieldOfWorkButton.snp.makeConstraints { make in + make.top.equalTo(fieldOfWork.snp.bottom).offset(18) + make.leading.trailing.equalToSuperview().inset(10) + make.width.equalToSuperview().multipliedBy(0.8) + make.height.equalTo(fieldOfWorkButton.snp.width).multipliedBy(0.1) + } + + fieldOfWorkDownImage.snp.makeConstraints { make in + make.centerY.equalTo(fieldOfWorkButton.snp.centerY) + make.height.width.equalTo(fieldOfWorkButton.snp.height).multipliedBy(0.8) + make.trailing.equalToSuperview().inset(10) + } + + fieldOfWorkLine.snp.makeConstraints { make in + make.top.equalTo(fieldOfWorkButton.snp.bottom).offset(8) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(1) + } + + /// 직업 + job.snp.makeConstraints { make in + make.top.equalTo(fieldOfWorkLine.snp.bottom).offset(50) + make.leading.equalToSuperview().offset(10) + } + jobTextField.snp.makeConstraints { make in + make.top.equalTo(job.snp.bottom).offset(18) + make.leading.trailing.equalToSuperview().inset(10) + make.width.equalToSuperview().multipliedBy(0.8) + } + + checkLenghtJobLabel.snp.makeConstraints { make in + make.trailing.equalTo(jobLine.snp.trailing) + make.centerY.equalTo(jobTextField.snp.centerY) + } + + jobLine.snp.makeConstraints { make in + make.top.equalTo(jobTextField.snp.bottom).offset(8) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(1) + } + + /// 연차 + annual.snp.makeConstraints { make in + make.top.equalTo(jobLine.snp.bottom).offset(50) + make.leading.equalToSuperview().offset(10) + } + annualButton.snp.makeConstraints { make in + make.top.equalTo(annual.snp.bottom).offset(18) + make.leading.equalToSuperview().inset(10) + make.width.equalToSuperview().multipliedBy(0.8) + make.height.equalTo(annualButton.snp.width).multipliedBy(0.1) + + } + annualDownImage.snp.makeConstraints { make in + make.centerY.equalTo(annualButton.snp.centerY) + make.height.width.equalTo(annualButton.snp.height).multipliedBy(0.8) + make.trailing.equalToSuperview().inset(10) + } + + annualLine.snp.makeConstraints { make in + make.top.equalTo(annualButton.snp.bottom).offset(8) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(1) + + } + nickNameExplainLabel.snp.makeConstraints { make in + make.bottom.equalTo(checkButton.snp.top).offset(-20) + make.leading.trailing.equalToSuperview().inset(10) + } + + checkButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.height.equalTo(checkButtonView.snp.width).multipliedBy(0.15) + make.leading.trailing.equalToSuperview().inset(17) + + checkButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = ProfileSettingViewModel.Input(startButtonTapped: checkButton.rx.tap.asObservable(), + jobTextChanged: jobTextField.rx.text.orEmpty.asObservable()) + let output = viewModel.bind(input: input) + + /// 글자수 출력 바인딩 + output.jobLength + .map { "(\($0)/30)" } + .bind(to: checkLenghtJobLabel.rx.text) + .disposed(by: disposeBag) + + // 버튼 활성화 상태 및 색상 변경 바인딩 + output.isCheckButtonEnabled + .observe(on: MainScheduler.instance) + .bind { [weak self] isEnabled in + guard let self = self else { return } + checkButton.isEnabled = isEnabled + checkButtonView.layer.borderColor = UIColor.OnOffMain.cgColor + checkButtonView.layer.borderWidth = 1 + + checkButtonView.backgroundColor = isEnabled ? UIColor.OnOffMain : .white + checkButton.setTitleColor(isEnabled ? .white : UIColor.OnOffMain, for: .normal) + } + .disposed(by: disposeBag) + + output.success + .filter { $0 } + .subscribe(onNext: { [weak self] _ in + self?.moveToSelectTime() + }) + .disposed(by: disposeBag) + + checkButton.rx.tap + .bind { [weak self] in + if let job = self?.jobTextField.text { + _ = KeychainWrapper.saveItem(value: job, forKey: ProfileKeyChain.job.rawValue) + } + } + .disposed(by: disposeBag) + + fieldOfWorkButton.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.presentModalForProfileSetting(dataType: .fieldOfWork) + }) + .disposed(by: disposeBag) + + annualButton.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.presentModalForProfileSetting(dataType: .experienceYear) + }) + .disposed(by: disposeBag) + } + + /// 이모티콘 모달 띄우기 + private func presentModalForProfileSetting(dataType: ProfileDataType) { + let viewModel = ModalSelectProfileViewModel() + let modalSelectProfileViewController = ModalSelectProfileViewController(viewModel: viewModel, dataType: dataType) + modalSelectProfileViewController.delegate = self + + if #available(iOS 15.0, *) { + if let sheet = modalSelectProfileViewController.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + } + } + present(modalSelectProfileViewController, animated: true, completion: nil) + } + + /// 프로필설정으로 이동 + private func moveToSelectTime() { + let selectTimeViewModel = SelectTimeViewModel() + let vc = SelectTimeViewController(viewModel: selectTimeViewModel) + self.navigationController?.pushViewController(vc, animated: true) + } +} + +/// extension : ModalSelectProfileDelegate : 모달창으로 부터 데이터 받기 +extension ProfileSettingViewController: ModalSelectProfileDelegate { + func optionSelected(data: String, dataType: ProfileDataType) { + switch dataType { + case .fieldOfWork: + self.fieldOfWorkButton.setTitle(data, for: .normal) + _ = KeychainWrapper.saveItem(value: data, forKey: ProfileKeyChain.fieldOfWork.rawValue) + case .experienceYear: + self.annualButton.setTitle(data, for: .normal) + _ = KeychainWrapper.saveItem(value: data, forKey: ProfileKeyChain.experienceYear.rawValue) + + } + } +} diff --git a/On_off_iOS/On_off_iOS/LogIn/ProfileSetting/ViewModel/ProfileSettingViewModel.swift b/On_off_iOS/On_off_iOS/LogIn/ProfileSetting/ViewModel/ProfileSettingViewModel.swift new file mode 100644 index 0000000..1a42ef6 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/ProfileSetting/ViewModel/ProfileSettingViewModel.swift @@ -0,0 +1,126 @@ +// +// ProfileSettingViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/01. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// ProfileSettingViewModel +final class ProfileSettingViewModel { + private let disposeBag = DisposeBag() + private let loginService: LoginService + + /// Input + struct Input { + let startButtonTapped: Observable + let jobTextChanged: Observable + } + + /// Output + struct Output { + let jobLength: PublishSubject = PublishSubject() + let isCheckButtonEnabled: BehaviorRelay = BehaviorRelay(value: true) + let success: PublishSubject = PublishSubject() + let errorMessage: PublishSubject = PublishSubject() + + } + + // MARK: - Init + init(loginService: LoginService) { + self.loginService = loginService + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + func bind(input: Input) -> Output { + let output = Output() + + // 닉네임 텍스트 변경 관찰 및 유효성 검사 + input.jobTextChanged + .map { nickName in + return nickName.count // 닉네임 길이만 반환 + } + .do(onNext: { length in + output.isCheckButtonEnabled.accept(length >= 2 && length <= 30) // 2자 이상 30자 이하 조건만 검사 + }) + .bind(to: output.jobLength) + .disposed(by: disposeBag) + + // 시작 버튼 탭 이벤트 처리 + input.startButtonTapped + .flatMapLatest { [weak self] _ -> Observable> in + guard let self = self else { + return .empty() + } + return self.loginWithSelectedData() + } + .subscribe(onNext: { response in + if response.isSuccess { + output.success.onNext(true) + } else { + output.errorMessage.onNext(response.message) + } + }, onError: { error in + output.errorMessage.onNext(error.localizedDescription) + }) + .disposed(by: disposeBag) + + return output + } + + private func loginWithSelectedData() -> Observable> { + + guard let loginMethod = KeychainWrapper.loadItem(forKey: LoginMethod.loginMethod.rawValue), + let nickname = KeychainWrapper.loadItem(forKey: ProfileKeyChain.nickname.rawValue), + let fieldOfWork = KeychainWrapper.loadItem(forKey: ProfileKeyChain.fieldOfWork.rawValue), + let job = KeychainWrapper.loadItem(forKey: ProfileKeyChain.job.rawValue), + let experienceYear = KeychainWrapper.loadItem(forKey: ProfileKeyChain.experienceYear.rawValue) + else { + return .empty() + } + + /// apple은 최초 이후엔 정보 optional로 nil 값 + if loginMethod == "apple" { + let oauthId = KeychainWrapper.loadItem(forKey: AppleLoginKeyChain.oauthId.rawValue) ?? "" + let givenName = KeychainWrapper.loadItem(forKey: AppleLoginKeyChain.giveName.rawValue) ?? "" + let familyName = KeychainWrapper.loadItem(forKey: AppleLoginKeyChain.familyName.rawValue) ?? "" + let email = KeychainWrapper.loadItem(forKey: AppleLoginKeyChain.email.rawValue) ?? "" + let identityTokenString = KeychainWrapper.loadItem(forKey: AppleLoginKeyChain.identityTokenString.rawValue) ?? "" + let authorizationCodeString = KeychainWrapper.loadItem(forKey: AppleLoginKeyChain.authorizationCodeString.rawValue) ?? "" + + let fullName = FullName(giveName: givenName, familyName: familyName) + + let additionalInfo = AdditionalInfo(nickname: nickname, fieldOfWork: fieldOfWork, job: job, experienceYear: experienceYear) + + let request = AppleTokenValidationRequest( + oauthId: oauthId, + fullName: fullName, + email: email, + identityToken: identityTokenString, + authorizationCode: authorizationCodeString, + additionalInfo: additionalInfo + ) + + return loginService.validateAppleTokenAndSendInfo(request: request) + } + + else if loginMethod == "kakao" { + guard let identityToken = KeychainWrapper.loadItem(forKey: KakaoLoginKeyChain.idToken.rawValue), + let accessToken = KeychainWrapper.loadItem(forKey: KakaoLoginKeyChain.accessToken.rawValue) + else { + return .empty() + } + + let additionalInfo = AdditionalInfo(nickname: nickname, fieldOfWork: fieldOfWork, job: job, experienceYear: experienceYear) + let request = KakaoTokenValidationRequest(identityToken: identityToken, accessToken: accessToken, additionalInfo: additionalInfo) + return loginService.validateKakaoTokenAndSendInfo(request: request) + } + return .empty() + } +} diff --git a/On_off_iOS/On_off_iOS/LogIn/Protocol/LoginProtocol.swift b/On_off_iOS/On_off_iOS/LogIn/Protocol/LoginProtocol.swift new file mode 100644 index 0000000..12f9e75 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Protocol/LoginProtocol.swift @@ -0,0 +1,22 @@ +// +// LoginProtocol.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import Foundation +import RxSwift +import KakaoSDKAuth +import KakaoSDKUser + +protocol LoginProtocol { + /// 카카오 로그인 + func kakaoLogin() -> Observable + + /// 로그인 API + /// - Parameter request: Kakao, Apple에서 발급받는 Token, AuthType + /// - Returns: status, Tokens + func validateKakaoTokenAndSendInfo(request: KakaoTokenValidationRequest) -> Observable> + func validateAppleTokenAndSendInfo(request: AppleTokenValidationRequest) -> Observable> +} diff --git a/On_off_iOS/On_off_iOS/LogIn/Protocol/SignUpProtocol.swift b/On_off_iOS/On_off_iOS/LogIn/Protocol/SignUpProtocol.swift new file mode 100644 index 0000000..38f56d3 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Protocol/SignUpProtocol.swift @@ -0,0 +1,16 @@ +// +// SignUpProtocol.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/01. +// + +import Foundation +import RxSwift +// +//protocol SignUpProtocol { +// /// 회원가입할 떄 호출 +// /// - Parameter request: 서버에 보내는 회원가입 정보 +// /// - Returns: AccesToken, RefreshToken +// func validateKakaoTokenAndSendInfo(request: KakaoTokenValidationRequest) -> Observable +//} diff --git a/On_off_iOS/On_off_iOS/LogIn/Service/LoginService.swift b/On_off_iOS/On_off_iOS/LogIn/Service/LoginService.swift new file mode 100644 index 0000000..d412620 --- /dev/null +++ b/On_off_iOS/On_off_iOS/LogIn/Service/LoginService.swift @@ -0,0 +1,202 @@ +// +// LoginService.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import Alamofire +import KakaoSDKAuth +import KakaoSDKCommon +import KakaoSDKUser +import RxKakaoSDKUser +import RxRelay +import RxSwift + +/// 로그인 Service +final class LoginService: LoginProtocol { + private let disposeBag = DisposeBag() + + /// 카카오 로그인 + func kakaoLogin() -> Observable { + return Observable.create { [weak self] observer in + if UserApi.isKakaoTalkLoginAvailable() { + UserApi.shared.rx.loginWithKakaoTalk() + .flatMap { _ in self?.fetchKakaoUserInfo() ?? .empty() } + .subscribe( + onNext: { userInfo in + observer.onNext(userInfo) + observer.onCompleted() + }, + onError: { error in + observer.onError(error) + } + ) + .disposed(by: self?.disposeBag ?? DisposeBag()) + return Disposables.create() + } + + UserApi.shared.rx.loginWithKakaoAccount() + .flatMap { _ in self?.fetchKakaoUserInfo() ?? .empty() } + .subscribe( + onNext: { userInfo in + observer.onNext(userInfo) + }, + onError: { error in + print("loginWithKakaoAccount() error: \(error)") + } + ) + .disposed(by: self?.disposeBag ?? DisposeBag()) + return Disposables.create() + } + } + + /// 카카오 사용자 정보 불러오기 + func fetchKakaoUserInfo() -> Observable { + return UserApi.shared.rx.me().asObservable() + .do(onNext: { user in + print("fetchKakaoUserInfo \n\(user)") + }, onError: { error in + print("fetchKakaoUserInfo error!\n\(error)") + }) + } + + /// 로그인 API + /// - Parameter request: Kakao에서 발급받는 Token + /// - Returns: Tokens + func validateKakaoTokenAndSendInfo(request: KakaoTokenValidationRequest) -> Observable> { + let url = Domain.RESTAPI + LoginPath.kakaoLogin.rawValue + let headers = Header.header.getHeader() + + return Observable.create { observer in + AF.request(url, method: .post, + parameters: request, + encoder: JSONParameterEncoder.default, + headers: headers) + .validate() + .responseDecodable(of: Response.self) { response in + + switch response.result { + + case .success(let data): + print("로그인 성공: \(response)") + observer.onNext(data) + _ = KeychainWrapper.saveItem(value: data.result.accessToken, forKey: LoginKeyChain.accessToken.rawValue) + _ = KeychainWrapper.saveItem(value: data.result.refreshToken, forKey: LoginKeyChain.refreshToken.rawValue) + + observer.onCompleted() + + case .failure(let error): + print("로그인 실패:") + observer.onError(error) + } + } + return Disposables.create() + } + } + + /// - Parameter request: Apple에서 발급받는 Token + /// - Returns: Tokens + func validateAppleTokenAndSendInfo(request: AppleTokenValidationRequest) -> Observable> { + let url = Domain.RESTAPI + LoginPath.appleLogin.rawValue + let headers = Header.header.getHeader() + + return Observable.create { observer in + AF.request(url, method: .post, + parameters: request, + encoder: JSONParameterEncoder.default, + headers: headers) + .validate() + .responseDecodable(of: Response.self) { response in + + switch response.result { + + case .success(let data): + print("👍로그인 성공: \(response)") + observer.onNext(data) + _ = KeychainWrapper.saveItem(value: data.result.accessToken, forKey: LoginKeyChain.accessToken.rawValue) + _ = KeychainWrapper.saveItem(value: data.result.refreshToken, forKey: LoginKeyChain.refreshToken.rawValue) + + observer.onCompleted() + + case .failure(let error): + print("로그인 실패:") + observer.onError(error) + } + } + return Disposables.create() + } + } + + /// 직업 정보 가져오기 + func fetchJobOptions() -> Observable> { + let url = Domain.RESTAPI + LoginPath.job.rawValue + return Observable.create { observer in + AF.request(url, method: .get) + .validate() + .responseDecodable(of: Response<[String]>.self) { response in + switch response.result { + case .success(let data): + observer.onNext(data) + observer.onCompleted() + case .failure(let error): + observer.onError(error) + } + } + return Disposables.create() + } + } + + /// 연차 정보 가져오기 + func fetchExperienceYearsOptions() -> Observable> { + let url = Domain.RESTAPI + LoginPath.experienceYear.rawValue + return Observable.create { observer in + AF.request(url, method: .get) + .validate() + .responseDecodable(of: Response<[String]>.self) { response in + switch response.result { + case .success(let data): + observer.onNext(data) + observer.onCompleted() + case .failure(let error): + observer.onError(error) + } + } + return Disposables.create() + } + } + + /// 유효성 검사하기 + /// - Parameter request: 서버에서 발급받는 Token + /// - Returns: Tokens + func validateTokenAndSendInfo(request: TokenResult) -> Observable> { + let url = Domain.RESTAPI + LoginPath.checkValidation.rawValue + let headers = Header.header.getHeader() + + return Observable.create { observer in + AF.request(url, method: .post, + parameters: request, + encoder: JSONParameterEncoder.default, + headers: headers) + .validate() + .responseDecodable(of: Response.self) { response in + + switch response.result { + + case .success(let data): + print("👍로그인 성공: \(response)") + observer.onNext(data) + _ = KeychainWrapper.saveItem(value: data.result.accessToken, forKey: LoginKeyChain.accessToken.rawValue) + _ = KeychainWrapper.saveItem(value: data.result.refreshToken, forKey: LoginKeyChain.refreshToken.rawValue) + observer.onCompleted() + + case .failure(let error): + print("로그인 실패:") + observer.onError(error) + } + } + return Disposables.create() + } + } + +} diff --git a/On_off_iOS/On_off_iOS/LogIn/View/LoginViewController.swift b/On_off_iOS/On_off_iOS/LogIn/View/LoginViewController.swift index e621b99..a90c822 100644 --- a/On_off_iOS/On_off_iOS/LogIn/View/LoginViewController.swift +++ b/On_off_iOS/On_off_iOS/LogIn/View/LoginViewController.swift @@ -8,8 +8,10 @@ import UIKit import RxSwift import RxCocoa +import AuthenticationServices +import RxGesture -///로그인 화면 +/// 로그인 화면 final class LoginViewController: UIViewController { private let welcomeLabel: UILabel = { @@ -18,35 +20,51 @@ final class LoginViewController: UIViewController { label.numberOfLines = 0 label.textAlignment = .left label.font = UIFont.systemFont(ofSize: 24, weight: .bold) + label.textColor = .white return label }() - /// 로그인 버튼 - private let kakaoLoginButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("카카오 로그인 ", for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 18) - return button + /// 이미지뷰 생성 및 설정 + private lazy var decorateImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "온보딩시작") + imageView.contentMode = .scaleAspectFit + imageView.isUserInteractionEnabled = true + return imageView + }() + /// 카카오 로그인 이미지뷰 생성 및 설정 + private lazy var kakaoLoginImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "kakao_login") // 카카오 로그인 이미지 설정 + imageView.contentMode = .scaleAspectFit + imageView.isUserInteractionEnabled = true + return imageView }() - private let appleLoginButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("애플 로그인", for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 18) - return button + /// 애플 로그인 이미지뷰 생성 및 설정 + private lazy var appleLoginImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "apple_login") // 애플 로그인 이미지 설정 + imageView.contentMode = .scaleAspectFit + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onAppleLoginImageViewTapped)) + imageView.addGestureRecognizer(tapGesture) + imageView.isUserInteractionEnabled = true + return imageView }() /// 이용약관 라벨 private let termsLabel: UILabel = { let label = UILabel() label.text = "이용약관 및 개인정보 처리방침" - label.font = UIFont.systemFont(ofSize: 16) - label.textColor = .lightGray + label.font = UIFont.systemFont(ofSize: 10) + label.textColor = .white return label }() private let viewModel: LoginViewModel private let disposeBag = DisposeBag() + private let appleLoginSuccessSubject = PublishSubject() + // MARK: - Init init(viewModel: LoginViewModel) { @@ -62,16 +80,22 @@ final class LoginViewController: UIViewController { // MARK: - ViewDidLoad override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white + settingUI() addSubviews() setupBindings() } + /// settingUI + private func settingUI(){ + view.backgroundColor = UIColor.OnOffMain + } + /// addSubviews private func addSubviews(){ view.addSubview(welcomeLabel) - view.addSubview(kakaoLoginButton) - view.addSubview(appleLoginButton) + view.addSubview(decorateImageView) + view.addSubview(kakaoLoginImageView) + view.addSubview(appleLoginImageView) view.addSubview(termsLabel) configureConstraints() } @@ -83,34 +107,157 @@ final class LoginViewController: UIViewController { make.top.equalTo(view.safeAreaLayoutGuide).offset(100) make.leading.equalToSuperview().offset(50) } - - kakaoLoginButton.snp.makeConstraints { make in - make.centerY.equalToSuperview().offset(50) + decorateImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.height.width.equalTo(view.snp.width).multipliedBy(0.8) + } + kakaoLoginImageView.snp.makeConstraints { make in make.centerX.equalToSuperview() + make.top.equalTo(decorateImageView.snp.bottom).offset(-20) + make.width.equalToSuperview().multipliedBy(0.8) + make.height.equalTo(kakaoLoginImageView.snp.width).multipliedBy(0.18) } - appleLoginButton.snp.makeConstraints { make in - make.top.equalTo(kakaoLoginButton.snp.bottom).offset(20) + appleLoginImageView.snp.makeConstraints { make in + make.top.equalTo(kakaoLoginImageView.snp.bottom).offset(20) + make.width.height.equalTo(kakaoLoginImageView) make.centerX.equalToSuperview() } termsLabel.snp.makeConstraints { make in - make.bottom.equalToSuperview().inset(20) - make.height.equalTo(view.snp.width).multipliedBy(0.1) + make.top.equalTo(appleLoginImageView.snp.bottom).offset(20) make.centerX.equalToSuperview() } + } + + /// 애플 로그인 과정을 시작 + @objc + private func onAppleLoginImageViewTapped() { + let appleIDProvider = ASAuthorizationAppleIDProvider() + let request = appleIDProvider.createRequest() + request.requestedScopes = [.fullName, .email] //유저로 부터 알 수 있는 정보들(name, email) + + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.presentationContextProvider = self + authorizationController.performRequests() } - - /// ViewModel과 bind + + /// setupBindings : viewMode과l bind private func setupBindings() { + let input = LoginViewModel.Input( - kakaoButtonTapped: kakaoLoginButton.rx.tap.asObservable(), - appleButtonTapped: appleLoginButton.rx.tap.asObservable() + kakaoButtonTapped: kakaoLoginImageView.rx.tapGesture().when(.recognized).asObservable(), + appleLoginSuccess: appleLoginSuccessSubject.asObservable() // 애플 로그인 성공 이벤트를 Observable로 전달 + ) - viewModel.bind(input: input) + + let output = viewModel.bind(input: input) + + output.checkSignInService.subscribe(onNext: { signInStatus in + print("로그인 상태: \(String(describing: signInStatus))") + }) + .disposed(by: disposeBag) + + output.moveToNickName + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + print("🍎") + self?.moveToNickName() + }) + .disposed(by: disposeBag) + + output.moveToMain + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + print("🍎") + self?.moveToMain() + }) + .disposed(by: disposeBag) + + + output.moveToBack + .subscribe(onNext: { [weak self] _ in + self?.navigationController?.popViewController(animated: false) + }) + .disposed(by: disposeBag) + } + /// 닉네임 설정으로 이동 + private func moveToNickName() { + print("이동해야함") + let nickNameViewModel = NickNameViewModel() + let nickNameViewController = NickNameViewController(viewModel: nickNameViewModel) + self.navigationController?.pushViewController(nickNameViewController, animated: true) } + /// 메인 화면으로 이동 + private func moveToMain() { + print("이동해야함") + let nickNameViewModel = NickNameViewModel() + let nickNameViewController = NickNameViewController(viewModel: nickNameViewModel) + self.navigationController?.pushViewController(nickNameViewController, animated: true) + } } - +// MARK: - extension :ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding +extension LoginViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding{ + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return self.view.window! + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + + //로그인 성공 + switch authorization.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + let userIdentifier = appleIDCredential.user + let givenName = appleIDCredential.fullName?.givenName ?? "" + let familyName = appleIDCredential.fullName?.familyName ?? "" + let email = appleIDCredential.email ?? "" + + ///identityToken, authorizationCode를 인코딩 + guard let identityToken = appleIDCredential.identityToken, + let authorizationCode = appleIDCredential.authorizationCode, + let identityTokenString = String(data: identityToken, encoding: .utf8), + let authorizationCodeString = String(data: authorizationCode, encoding: .utf8) else { + print("Error") + return + } + print(""" + { + "oauthId": \(userIdentifier), + "fullName": { + "givenName": \(givenName), + "familyName": \(familyName) + }, + "email": \(email), + "identityToken": \(identityTokenString), + "authorizationCode": \(authorizationCodeString), + "additionalInfo": { + "fieldOfWork": "부동산_임대업", + "job": "aa", + "experienceYear": "신입" + } + } + """) + // 키체인에 정보 저장 + _ = KeychainWrapper.saveItem(value: "apple", forKey: LoginMethod.loginMethod.rawValue) + + _ = KeychainWrapper.saveItem(value: userIdentifier, forKey: AppleLoginKeyChain.oauthId.rawValue) + _ = KeychainWrapper.saveItem(value: givenName, forKey: AppleLoginKeyChain.giveName.rawValue) + _ = KeychainWrapper.saveItem(value: familyName, forKey: AppleLoginKeyChain.familyName.rawValue) + _ = KeychainWrapper.saveItem(value: email, forKey: AppleLoginKeyChain.email.rawValue) + _ = KeychainWrapper.saveItem(value: identityTokenString, forKey: AppleLoginKeyChain.identityTokenString.rawValue) + _ = KeychainWrapper.saveItem(value: authorizationCodeString, forKey: AppleLoginKeyChain.authorizationCodeString.rawValue) + appleLoginSuccessSubject.onNext(()) + default: + break + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + // 로그인 실패(유저의 취소도 포함) + print("login failed - \(error.localizedDescription)") + } +} diff --git a/On_off_iOS/On_off_iOS/LogIn/ViewModel/LoginViewModel.swift b/On_off_iOS/On_off_iOS/LogIn/ViewModel/LoginViewModel.swift index 5175057..bd4361a 100644 --- a/On_off_iOS/On_off_iOS/LogIn/ViewModel/LoginViewModel.swift +++ b/On_off_iOS/On_off_iOS/LogIn/ViewModel/LoginViewModel.swift @@ -9,43 +9,110 @@ import RxCocoa import RxRelay import RxSwift import UIKit +import KakaoSDKAuth +import KakaoSDKCommon +import KakaoSDKUser /// LoginViewModel final class LoginViewModel { private let disposeBag = DisposeBag() - var navigationController: UINavigationController - + private var loginService: LoginService? + private var output: Output? + /// Input struct Input { - let kakaoButtonTapped: Observable - let appleButtonTapped: Observable + let kakaoButtonTapped: Observable.Element> + let appleLoginSuccess: Observable } - + + /// Output + struct Output { + var checkSignInService: BehaviorRelay = BehaviorRelay(value: nil) + let moveToMain = PublishSubject() + let moveToNickName = PublishSubject() + let moveToBack = PublishSubject() + } + // MARK: - Init - init(navigationController: UINavigationController) { - self.navigationController = navigationController + init(loginService: LoginService) { + self.loginService = loginService + } + + /// binding Input + /// - Parameters: + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = createOutput(input: input) + self.output = output + return output } - /// bind - /// - Parameter input: kakaoButtonTapped, appleButtonTapped - func bind(input: Input) { + /// create Output + /// - Parameters: + /// - input: Input 구조체 + /// - Returns: Output 구조체 + private func createOutput(input: Input) -> Output { + let output = Output() input.kakaoButtonTapped - .bind { [weak self] in - self?.moveToNickName() - } - .disposed(by: disposeBag) + .bind { [weak self] _ in + guard let self = self else {return} + print("called kakaoLogin") + kakaoLogin() + } + .disposed(by: disposeBag) + + input.appleLoginSuccess + .subscribe(onNext: { [weak self] _ in + output.moveToNickName.onNext(()) + }) + .disposed(by: disposeBag) + + return output + } + + // 서버 응답 처리 + // MARK: - 카카오 로그인 로직 + /// 카카오 로그인 처리 + /// - Parameters: + /// - oauthToken: OAuth 토큰 + /// - error: 발생한 에러 + private func handleKakaoLoginResult(oauthToken: OAuthToken?, error: Error?) { + + if let error = error { + print("Login Error: \(error.localizedDescription)") + return + } + + guard let oauthToken = oauthToken else { return } + + // Keychain에 토큰 정보 저장 + _ = KeychainWrapper.saveItem(value: "kakao", forKey: LoginMethod.loginMethod.rawValue) + + let saveAccessTokenSuccess = KeychainWrapper.saveItem(value: oauthToken.accessToken, forKey: KakaoLoginKeyChain.accessToken.rawValue) + let saveIdTokenSuccess = KeychainWrapper.saveItem(value: oauthToken.idToken ?? "", forKey: KakaoLoginKeyChain.idToken.rawValue) + + // 저장 성공 여부 확인 + if saveAccessTokenSuccess && saveIdTokenSuccess { + print("Access Token과 ID Token 저장 성공") + self.output?.moveToNickName.onNext(()) + } else { + print("토큰 저장 실패") + } - input.appleButtonTapped - .bind { [weak self] in - self?.moveToNickName() - } - .disposed(by: disposeBag) } - /// 닉네임 설정으로 이동 - private func moveToNickName() { - let vc = NickNameViewController(viewModel: NickNameViewModel(navigationController: navigationController)) - navigationController.pushViewController(vc, animated: true) + /// 카카오 로그인 수행 + private func kakaoLogin() { + if UserApi.isKakaoTalkLoginAvailable() { + UserApi.shared.loginWithKakaoTalk(completion: { [weak self] (oauthToken, error) in + self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error) + }) + } else { + UserApi.shared.loginWithKakaoAccount(completion: { [weak self] (oauthToken, error) in + self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error) + }) + } } } diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/CollectionView/EmoticonCollectionViewCell.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/CollectionView/EmoticonCollectionViewCell.swift new file mode 100644 index 0000000..0834e78 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/CollectionView/EmoticonCollectionViewCell.swift @@ -0,0 +1,45 @@ +// +// EmoticonCollectionViewCell.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import SnapKit +import UIKit +import Kingfisher + +final class EmoticonCollectionViewCell: UICollectionViewCell { + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// setupViews + private func setupViews() { + contentView.addSubview(imageView) + imageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + ///이미지 + func configure(with imageUrl: String) { + if let url = URL(string: imageUrl) { + imageView.kf.setImage(with: url) + + } + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/ModalEmoticonViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/ModalEmoticonViewController.swift new file mode 100644 index 0000000..a96bd1d --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/ModalEmoticonViewController.swift @@ -0,0 +1,116 @@ +// +// ModalEmoticonViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import RxSwift +import RxCocoa +import SnapKit +import UIKit + +final class ModalEmoticonViewController: UIViewController { + + /// imageView collectionView + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .white + collectionView.register(EmoticonCollectionViewCell.self, + forCellWithReuseIdentifier: CellIdentifier.EmoticonCollectionViewCell.rawValue) + collectionView.delegate = self + return collectionView + }() + + private let viewModel: ModalEmoticonViewModel + private let disposeBag = DisposeBag() + + var onImageSelected: ((String) -> Void)? + + weak var delegate: ModalEmoticonDelegate? + + // MARK: - Init + init(viewModel: ModalEmoticonViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + setupConstraints() + setupBindings() + } + + /// setupViews + private func setupViews() { + view.backgroundColor = .white + view.addSubview(collectionView) + } + + /// setupConstraints + private func setupConstraints() { + collectionView.snp.makeConstraints { make in + make.horizontalEdges.bottom.equalToSuperview().inset(10) + make.top.equalToSuperview().inset(50) + } + } + + private func setupBindings() { + + let viewDidLoad = Observable.just(()) + let imageSelected = collectionView.rx.modelSelected(Emoticon.self).asObservable() + + let input = ModalEmoticonViewModel.Input(viewDidLoad: viewDidLoad, imageSelected: imageSelected) + let output = viewModel.bind(input: input) + + collectionView.rx.modelSelected(Emoticon.self) + .subscribe(onNext: { [weak self] emoticon in + + self?.delegate?.emoticonSelected(emoticon: emoticon) + self?.dismiss(animated: true, completion: nil) + }) + .disposed(by: disposeBag) + + + output.emoticons + .bind(to: collectionView.rx.items(cellIdentifier: CellIdentifier.EmoticonCollectionViewCell.rawValue, cellType: EmoticonCollectionViewCell.self)) { index, emoticon, cell in + cell.configure(with: emoticon.imageUrl) + } + .disposed(by: disposeBag) + } +} + +/// extension UICollectionViewDelegateFlowLayout +extension ModalEmoticonViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + // 한 줄에 3개의 이미지가 들어가도록 너비 계산 + let paddingSpace = 10 * (3 + 1) + let availableWidth = collectionView.frame.width - CGFloat(paddingSpace) - collectionView.contentInset.left - collectionView.contentInset.right + let widthPerItem = availableWidth / 3 + + return CGSize(width: widthPerItem, height: widthPerItem) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 10 + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/ModalEmoticonViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/ModalEmoticonViewModel.swift new file mode 100644 index 0000000..2f59f8f --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/Emoticon/ModalEmoticonViewModel.swift @@ -0,0 +1,60 @@ +// +// ModalEmoticonViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import RxCocoa +import RxSwift + +final class ModalEmoticonViewModel { + + private let disposeBag = DisposeBag() + private let memoirsService = MemoirsService() + + /// Input + struct Input { + + /// 뷰가 로드될 때 이벤트 처리 + let viewDidLoad: Observable + + /// 이미지 선택 이벤트를 처리 + let imageSelected: Observable + } + + /// Output + struct Output { + let emoticons: Observable<[Emoticon]> + } + + /// bind + /// - Parameter input: input + /// - Returns: output + /// 입력을 받아 처리하고 출력을 반환 + func bind(input: Input) -> Output { + + /// 뷰 로드 시 + let emoticons = input.viewDidLoad + .flatMapLatest { [weak self] _ -> Observable<[Emoticon]> in + guard let self = self else { return .empty() } + return fetchEmoticons() + } + + /// 이미지 선택 이벤트 처리 + input.imageSelected + .subscribe(onNext: { emoticon in + print("선택된 emoticon: \(emoticon.emoticonId), URL: \(emoticon.imageUrl)") + }) + .disposed(by: disposeBag) + + return Output(emoticons: emoticons) + } + + /// 서버에서 이모티콘 데이터 + private func fetchEmoticons() -> Observable<[Emoticon]> { + return memoirsService.getEmoticon() + .map { $0.result } + .catchAndReturn([]) + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconModel.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconModel.swift new file mode 100644 index 0000000..62d2a2e --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconModel.swift @@ -0,0 +1,21 @@ +// +// ExpressedIconModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/28. +// + +import Foundation + +///이모티콘 정보 +struct EmoticonResponse: Codable { + let isSuccess: Bool + let code: String + let message: String + let result: [Emoticon] +} + +struct Emoticon: Codable { + let emoticonId: Int + let imageUrl: String +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconViewController.swift new file mode 100644 index 0000000..c621b30 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconViewController.swift @@ -0,0 +1,240 @@ +// +// ExpressedIconViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/28. +// + +import UIKit +import RxSwift +import RxCocoa +//import SVGKit + +/// ExpressedIconViewController +final class ExpressedIconViewController: UIViewController { + + /// customBackButton + private let backButton : UIBarButtonItem = { + let button = UIBarButtonItem(title: MemoirsText.getText(for: .backButton), style: .plain, target: nil, action: nil) + button.tintColor = .black + return button + }() + + /// pageControl + private lazy var pageControlImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.PageControl5.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// welcomeLabel + private let welcomeLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .iconSelection) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 회고록 작성페이지 그림 + private lazy var textpageImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.TextpageImage4.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 이모티콘 이미지 + private lazy var emoticonImage: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + /// 확인 버튼 + private let saveButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("저장하기", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + button.titleLabel?.tintColor = .OnOffMain + + return button + }() + + + /// 확인 버튼 뷰 + private lazy var saveButtonView: UIView = { + let view = UIView() + view.backgroundColor = .OnOffMain + // 초기 버튼 상태 비활성화 + view.layer.borderColor = UIColor.OnOffMain.cgColor + view.layer.borderWidth = 1 + view.backgroundColor = .white + return view + }() + + private let viewModel: ExpressedIconViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: ExpressedIconViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + settingView() + addSubviews() + setupBindings() + settingCheckButtonView() + } + + private func settingView(){ + view.backgroundColor = .OnOffLightMain + } + + /// 확인 버튼 속성 설정 + private func settingCheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .longButtonCornerRadius, width: view.frame.width) + saveButtonView.layer.cornerRadius = cornerRadius + saveButtonView.layer.masksToBounds = true + } + + /// addSubviews + private func addSubviews() { + + view.addSubview(pageControlImage) + view.addSubview(welcomeLabel) + + view.addSubview(textpageImage) + view.addSubview(emoticonImage) + + view.addSubview(saveButtonView) + saveButtonView.addSubview(saveButton) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints() { + + self.navigationItem.leftBarButtonItem = backButton + + pageControlImage.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.width.equalTo(view.snp.width).multipliedBy(0.25) + make.top.equalTo(view.safeAreaLayoutGuide).inset(10) + make.height.equalTo(pageControlImage.snp.width).multipliedBy(0.1) + } + + welcomeLabel.snp.makeConstraints { make in + make.top.equalTo(pageControlImage.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + textpageImage.snp.makeConstraints { make in + make.top.equalTo(welcomeLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(view.snp.height).multipliedBy(0.3) + } + + emoticonImage.snp.makeConstraints { make in + make.center.equalTo(textpageImage.snp.center) + make.height.width.equalTo(textpageImage.snp.height).multipliedBy(0.8) + } + + saveButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.width.equalTo(view.snp.width).multipliedBy(0.8) + make.height.equalTo(saveButtonView.snp.width).multipliedBy(0.15) + make.centerX.equalToSuperview() + } + + saveButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = ExpressedIconViewModel.Input(startButtonTapped: saveButton.rx.tap.asObservable(), + backButtonTapped: backButton.rx.tap.asObservable()) + + // 이미지 뷰 탭 제스처에 대한 바인딩 + textpageImage.rx.tapGesture() + .when(.recognized) + .subscribe(onNext: { _ in + self.presentModalEmoticonViewController() + }) + .disposed(by: disposeBag) + + let output = viewModel.bind(input: input) + + + output.moveToNext + .subscribe(onNext: { [weak self] in + self?.navigateTobookMark() + }) + .disposed(by: disposeBag) + + output.moveToBack + .subscribe(onNext: { [weak self] in + self?.navigationController? + .popViewController(animated: false)}) + + } + /// 임시 초기로 이동 + private func navigateTobookMark() { + let bookmarkViewModel = BookmarkViewModel() + let writePraisedViewController = BookmarkViewController(viewModel: bookmarkViewModel) + self.navigationController?.pushViewController(writePraisedViewController, animated: false) + } + + /// 이모티콘 모달 띄우기 + private func presentModalEmoticonViewController() { + let modalEmoticonViewController = ModalEmoticonViewController(viewModel: ModalEmoticonViewModel()) + modalEmoticonViewController.delegate = self + modalEmoticonViewController.onImageSelected = { [weak self] imageUrl in + self?.emoticonImage.kf.setImage(with: URL(string: imageUrl)) + } + + if #available(iOS 15.0, *) { + if let sheet = modalEmoticonViewController.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + } + } + present(modalEmoticonViewController, animated: true, completion: nil) + } +} + +extension ExpressedIconViewController: ModalEmoticonDelegate { + func emoticonSelected(emoticon: Emoticon) { + self.emoticonImage.kf.setImage(with: URL(string: emoticon.imageUrl), + completionHandler: { [weak self] result in + switch result { + case .success(_): + DispatchQueue.main.async { + // 이미지 로드 성공 시 버튼 활성화 + _ = KeychainWrapper.saveItem(value: String(emoticon.emoticonId), + forKey: MemoirsKeyChain.emoticonID.rawValue) + self?.saveButton.isEnabled = true + self?.saveButtonView.backgroundColor = .OnOffMain + self?.saveButton.setTitleColor(.white, for: .normal) + } + case .failure(_): + // 이미지 로드 실패 시 버튼 비활성화 + self?.saveButton.isEnabled = false + self?.saveButtonView.backgroundColor = .lightGray + self?.saveButton.setTitleColor(.gray, for: .normal) + } + }) + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconViewModel.swift new file mode 100644 index 0000000..436216a --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ExpressedIconViewModel.swift @@ -0,0 +1,83 @@ +// +// ExpressedIconViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/28. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// ExpressedIconViewModel +final class ExpressedIconViewModel { + private let disposeBag = DisposeBag() + private let memoirsService = MemoirsService() + + /// Input + struct Input { + let startButtonTapped: Observable + let backButtonTapped: Observable + } + + /// Output + struct Output { + let textLength: PublishSubject = PublishSubject() + let moveToNext = PublishSubject() + let moveToBack = PublishSubject() + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = Output() + + /// 완료버튼 클릭 + input.startButtonTapped + .flatMapLatest { [weak self] _ -> Observable in + guard let self = self else { return .just(false) } + return self.sendMemoirsData() + } + + .subscribe(onNext: { success in + if success { + output.moveToNext.onNext(()) + } + }) + .disposed(by: disposeBag) + + /// 뒤로가기 버튼 클릭 + input.backButtonTapped + .bind(to: output.moveToBack) + .disposed(by: disposeBag) + + return output + } + + private func sendMemoirsData() -> Observable { + let answer1 = KeychainWrapper.loadItem(forKey: MemoirsKeyChain.MemoirsAnswer1.rawValue) ?? "" + let answer2 = KeychainWrapper.loadItem(forKey: MemoirsKeyChain.MemoirsAnswer2.rawValue) ?? "" + let answer3 = KeychainWrapper.loadItem(forKey: MemoirsKeyChain.MemoirsAnswer3.rawValue) ?? "" + let emoticonId = KeychainWrapper.loadItem(forKey: MemoirsKeyChain.emoticonID.rawValue) ?? "" + let today = Date() + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let dateString = formatter.string(from: today) + + let request = MemoirRequest( + date: dateString, + emoticonId: Int(emoticonId) ?? 1, // 이모티콘 ID -> 수정 + memoirAnswerList: [ + MemoirRequest.MemoirAnswer(questionId: 1, answer: answer1), + MemoirRequest.MemoirAnswer(questionId: 2, answer: answer2), + MemoirRequest.MemoirAnswer(questionId: 3, answer: answer3) + ] + ) + return memoirsService.saveMemoirs(request: request) + .map { _ -> Bool in true } + .catchAndReturn(false) + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ModalEmoticonDelegate.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ModalEmoticonDelegate.swift new file mode 100644 index 0000000..9ba5096 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/ModalEmoticonDelegate.swift @@ -0,0 +1,12 @@ +// +// ModalEmoticonDelegate.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/04. +// + +import Foundation + +protocol ModalEmoticonDelegate: AnyObject { + func emoticonSelected(emoticon: Emoticon) +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/s.swift b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/s.swift new file mode 100644 index 0000000..6c219a4 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/ExpressedIcon/s.swift @@ -0,0 +1,14 @@ +// +// EmoticonSelectionDelegate.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/04. +// + +import Foundation + +/// EmoticonSelectionDelegate +protocol EmoticonSelectionDelegate: AnyObject { + func emoticonSelected(emoticonId: Int, imageUrl: String) +} + diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsComplete/MemoirsCompleteViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsComplete/MemoirsCompleteViewModel.swift new file mode 100644 index 0000000..4b64e4f --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsComplete/MemoirsCompleteViewModel.swift @@ -0,0 +1,64 @@ +// +// MemoirsCompleteViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/29. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +// MemoirsCompleteViewModel +final class MemoirsCompleteViewModel { + private let disposeBag = DisposeBag() + + /// Input + struct Input { + let completedButtonTapped: Observable + let backButtonTapped: Observable + } + + /// Output + struct Output { + let textLength: PublishSubject = PublishSubject() + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = Output() + + /// 완료버튼 클릭 + input.completedButtonTapped + .bind { [weak self] in + self?.moveToImprovement() + } + .disposed(by: disposeBag) + + /// 뒤로가기 버튼 클릭 + input.backButtonTapped + .bind { [weak self] in + guard let self = self else { return } + moveToBack() + } + .disposed(by: disposeBag) + + return output + } + + /// Memoirs 초기 화면으로 이동 + private func moveToImprovement() { + + } + + /// 뒤로 이동 + private func moveToBack() { +// navigationController.popViewController(animated: false) + } +} + + diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsComplete/ViewController/MemoirsCompleteViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsComplete/ViewController/MemoirsCompleteViewController.swift new file mode 100644 index 0000000..f0312e6 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsComplete/ViewController/MemoirsCompleteViewController.swift @@ -0,0 +1,140 @@ +// +// MemoirsCompleteViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/29. +// + +import UIKit +import RxSwift +import RxCocoa + +/// MemoirsCompleteViewController +final class MemoirsCompleteViewController: UIViewController { + + /// customBackButton + private let backButton : UIBarButtonItem = { + let button = UIBarButtonItem(title: MemoirsText.getText(for: .backButton), style: .plain, target: nil, action: nil) + button.tintColor = .black + return button + }() + + /// welcomeLabel + private let welcomeLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .memorialCompleted) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + label.textColor = .white + return label + }() + + /// 회고록 작성페이지 그림 + private lazy var MemoirsCompleteImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.MemoirsCompleteImage.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 확인 버튼 + private let saveButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("오늘의 회고 완료", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + button.titleLabel?.tintColor = .white + return button + }() + + /// 확인 버튼 뷰 + private lazy var saveButtonView: UIView = { + let view = UIView() + view.backgroundColor = .OnOffLightMain + return view + }() + + private let viewModel: MemoirsCompleteViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: MemoirsCompleteViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + setupUI() + addSubviews() + setupBindings() + settingCheckButtonView() + } + + private func setupUI() { + view.backgroundColor = .OnOffMain + } + + /// 확인 버튼 속성 설정 + private func settingCheckButtonView() { + let cornerRadius = UICalculator.calculate(for: .longButtonCornerRadius, width: view.frame.width) + saveButtonView.layer.cornerRadius = cornerRadius + saveButtonView.layer.masksToBounds = true + } + + /// addSubviews + private func addSubviews() { + + view.addSubview(MemoirsCompleteImage) + view.addSubview(welcomeLabel) + + view.addSubview(saveButtonView) + saveButtonView.addSubview(saveButton) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints() { + + self.navigationItem.leftBarButtonItem = backButton + MemoirsCompleteImage.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(20) + make.height.equalTo(MemoirsCompleteImage.snp.width).multipliedBy(0.6) + make.center.equalToSuperview() + } + + saveButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.width.equalTo(view.snp.width).multipliedBy(0.8) + make.height.equalTo(saveButtonView.snp.width).multipliedBy(0.15) + make.centerX.equalToSuperview() + } + + saveButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + welcomeLabel.snp.makeConstraints { make in + make.bottom.equalTo(saveButtonView.snp.top).offset(-30) + make.centerX.equalToSuperview() + } + + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = MemoirsCompleteViewModel.Input(completedButtonTapped: saveButton.rx.tap.asObservable(), + backButtonTapped: backButton.rx.tap.asObservable()) + + let _ = viewModel.bind(input: input) + + } +} + diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsConstants/MemoirsImage.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsConstants/MemoirsImage.swift new file mode 100644 index 0000000..7ff5c6d --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsConstants/MemoirsImage.swift @@ -0,0 +1,32 @@ +// +// MemoirsImage.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/03. +// + +import Foundation +import UIKit + +/// 회고록에서 사용되는 이미지 +enum MemoirsImage: String { + case bookmark + case ellipsis + + case PageControl1 + case StartToWriteImage + case TextpageImage1 + + case PageControl2 + + case PageControl3 + case TextpageImage2 + + case PageControl4 + case TextpageImage3 + + case PageControl5 + case TextpageImage4 + + case MemoirsCompleteImage +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsConstants/MemoirsText.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsConstants/MemoirsText.swift new file mode 100644 index 0000000..a48f58c --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsConstants/MemoirsText.swift @@ -0,0 +1,68 @@ +// +// MemoirsText.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/28. +// + +import Foundation + +/// 회고록의 인삿말 +struct MemoirsText { + enum TextType { + + /// custom backButton + case backButton + + /// 회고록 보는 페이지 + case learnedToday + case praiseToday + case improveFuture + + case encouragement + case dailyReflection + case praise + case selfPraisePrompt + case difficultyPrompt + case improvementPrompt + case iconSelection + case memorialCompleted + } + + /// 회고록의 인삿말 + /// - Parameter type: TextType + /// - Returns: String + static func getText(for type: TextType) -> String { + + switch type { + case .backButton: + return "〈 뒤로가기" + + case .learnedToday: + return "오늘 배운 점" + case .praiseToday: + return "오늘 칭찬할 점" + case .improveFuture: + return "앞으로 개선할 점" + + case .encouragement: + return "오늘 하루도 수고했어요\n회고로 이제 일에서 완전히 OFF 하세요" + + case .dailyReflection: + return "오늘 어떤 일을 했나요? 배운 게 있나요?" + case .praise: + return "고생했어요" + case .selfPraisePrompt: + return "스스로에게 칭찬 한 마디를 쓴다면?" + case .difficultyPrompt: + return "어려웠다거나 아쉬운 것도 있나요?" + case .improvementPrompt: + return "다음엔 더 잘할 수 있을거예요" + case .iconSelection: + return "오늘을 표현할 수 있는 아이콘을 골라주세요\n오늘의 감정이나 인상적인 일을 떠올려보세요" + case .memorialCompleted: + return "오늘도 퇴근 완료!\n이제 퇴근 후 일상을 즐겨보세요" + + } + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsService.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsService.swift new file mode 100644 index 0000000..4477b98 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsService.swift @@ -0,0 +1,65 @@ +// +// MemoirsService.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import Alamofire +import RxSwift +import UIKit + +final class MemoirsService: MemoirsProtocol { + + private let disposeBag = DisposeBag() + + /// 회고록 저장하기 + func saveMemoirs(request: MemoirRequest) -> RxSwift.Observable { + let url = Domain.RESTAPI + MemoirsPath.memoirsSave.rawValue + let headers = Header.header.getHeader() + + print(request) + return Observable.create { observer in + AF.request(url, + method: .post, + parameters: request, encoder: JSONParameterEncoder.default, headers: headers) + .validate(statusCode: 200..<201) + .responseDecodable(of: MemoirResponse.self) { response in + + switch response.result { + case .success(let data): + observer.onNext(data) + case .failure(let error): + observer.onError(error) + } + } + return Disposables.create() + } + } + + /// 이모티콘 띄우기 + func getEmoticon() -> RxSwift.Observable { + let url = Domain.RESTAPI + MemoirsPath.getEmoticon.rawValue + print(url) + let headers = Header.header.getHeader() + return Observable.create { observer in + AF.request(url, + method: .get, + headers: headers) + .validate(statusCode: 200..<201) + .responseDecodable(of: EmoticonResponse.self) { response in + + switch response.result { + case .success(let data): + print(data) + observer.onNext(data) + case .failure(let error): + observer.onError(error) + } + } + + return Disposables.create() + } + } +} + diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsViewController.swift new file mode 100644 index 0000000..792a706 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsViewController.swift @@ -0,0 +1,276 @@ +// +// MemoirsViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import UIKit +import RxSwift +import RxCocoa + +/// 회고록 설정 +final class MemoirsViewController: UIViewController { + + /// 북마크 버튼 - 네비게이션 바 + private lazy var bookmarkButton: UIBarButtonItem = { + let button = UIBarButtonItem(image: UIImage(systemName: MemoirsImage.bookmark.rawValue), style: .plain, target: nil, action: nil) + button.rx.tap + .subscribe(onNext: { [weak self] in + print("북마크 로직 구현") + }) + .disposed(by: disposeBag) + return button + }() + + /// 메뉴 버튼 - 네비게이션 바 + private lazy var menuButton: UIBarButtonItem = { + let button = UIBarButtonItem(image: UIImage(systemName: MemoirsImage.ellipsis.rawValue)?.rotated(by: .pi / 2), style: .plain, target: nil, action: nil) + button.rx.tap + .subscribe(onNext: { [weak self] in + print("메뉴 로직 구현") + }) + .disposed(by: disposeBag) + return button + }() + + /// 전체 스크롤 뷰 + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.addSubview(contentView) + return scrollView + }() + + /// scrollView 내부 contentView + private lazy var contentView: UIView = { + let view = UIView() + // 세 개의 뷰를 여기에 추가 + return view + }() + + /// systemImage +버튼 + private lazy var writeButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "plus"), for: .normal) + button.tintColor = .white + button.backgroundColor = .OnOffMain + button.layer.cornerRadius = 25 + button.layer.masksToBounds = true + return button + }() + + /// emoticon View + private lazy var emoticonView: UIView = { + let label = UILabel() + return label + }() + + /// emoticon 이미지 + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "AppIcon") + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// date label + private lazy var dateLabel: UILabel = { + let label = UILabel() + label.text = "날짜 정보" + label.font = UIFont.systemFont(ofSize: 16, weight: .regular) + return label + }() + + /// 오늘 배운 점 label + private lazy var learnedLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .learnedToday) + label.font = UIFont.systemFont(ofSize: 16, weight: .regular) + return label + }() + + /// 오늘 배운 점 View + private lazy var learnedView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "회고록글보기이미지") + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 칭찬할 점 label + private lazy var praisedLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .praiseToday) + label.font = UIFont.systemFont(ofSize: 16, weight: .regular) + return label + }() + + /// 칭찬할 점 View + private lazy var praisedView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "회고록글보기이미지") + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 개선할 점 label + private lazy var improvementLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .improveFuture) + label.font = UIFont.systemFont(ofSize: 16, weight: .regular) + return label + }() + + /// 개선할 점 View + private lazy var improvementView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "회고록글보기이미지") + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private var viewModel: MemoirsViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: MemoirsViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + setupView() + addSubviews() + setupBindings() + } + + /// 화면 설정 관련 함수 + private func setupView(){ + view.backgroundColor = .OnOffLightMain + } + + /// addSubviews + private func addSubviews(){ + setupNavigationBar() + view.addSubview(scrollView) + view.addSubview(writeButton) + + contentView.addSubview(emoticonView) + emoticonView.addSubview(imageView) + + contentView.addSubview(dateLabel) + + contentView.addSubview(learnedLabel) + contentView.addSubview(learnedView) + + contentView.addSubview(praisedLabel) + contentView.addSubview(praisedView) + + contentView.addSubview(improvementLabel) + contentView.addSubview(improvementView) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints(){ + + scrollView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.width.equalToSuperview() + } + + writeButton.snp.makeConstraints { make in + make.bottom.equalTo(view.snp.bottom).inset(50) + make.trailing.equalTo(view.snp.trailing).inset(50) + make.width.height.equalTo(50) + } + + emoticonView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(15) + make.leading.trailing.equalToSuperview() + make.height.equalTo(emoticonView.snp.width).multipliedBy(0.4) + } + + imageView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.height.equalTo(emoticonView.snp.height).multipliedBy(0.8) + } + + dateLabel.snp.makeConstraints { make in + make.top.equalTo(emoticonView.snp.bottom).offset(10) + make.leading.equalToSuperview().inset(15) + } + + learnedLabel.snp.makeConstraints { make in + make.top.equalTo(dateLabel.snp.bottom).offset(10) + make.leading.equalToSuperview().inset(15) + } + + learnedView.snp.makeConstraints { make in + make.top.equalTo(learnedLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(learnedView.snp.width).multipliedBy(0.4) + } + + praisedLabel.snp.makeConstraints { make in + make.top.equalTo(learnedView.snp.bottom).offset(18) + make.leading.equalToSuperview().inset(15) + } + + praisedView.snp.makeConstraints { make in + make.top.equalTo(praisedLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(praisedView.snp.width).multipliedBy(0.4) + } + + improvementLabel.snp.makeConstraints { make in + make.top.equalTo(praisedView.snp.bottom).offset(18) + make.leading.equalToSuperview().inset(15) + } + + improvementView.snp.makeConstraints { make in + make.top.equalTo(improvementLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(improvementView.snp.width).multipliedBy(0.4) + make.bottom.equalToSuperview().inset(20) + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = MemoirsViewModel.Input(bookMarkButtonTapped: bookmarkButton.rx.tap.asObservable(), + menuButtonTapped: menuButton.rx.tap.asObservable(), + writeButtonTapped: writeButton.rx.tap.asObservable()) + + let output = viewModel.bind(input: input) + + output.shouldNavigateToStartToWrite + .subscribe(onNext: { [weak self] _ in + self?.navigateToStartToWrite() + }) + .disposed(by: disposeBag) + } + + private func navigateToStartToWrite() { + let startToWriteViewModel = StartToWriteViewModel() + let startToWriteViewController = StartToWriteViewController(viewModel: startToWriteViewModel) + self.navigationController?.pushViewController(startToWriteViewController, animated: false) + } + + /// 네비게이션 바 + private func setupNavigationBar() { + navigationItem.rightBarButtonItems = [menuButton, bookmarkButton] + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/MemoirsViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/MemoirsViewModel.swift new file mode 100644 index 0000000..01d8060 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/MemoirsViewModel.swift @@ -0,0 +1,55 @@ +// +// MemoirsViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// MemoirsViewModel +final class MemoirsViewModel { + private let disposeBag = DisposeBag() + + /// Input + struct Input { + let bookMarkButtonTapped: Observable + let menuButtonTapped: Observable + let writeButtonTapped: Observable + } + + /// Output + struct Output { + let shouldNavigateToStartToWrite: Observable + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + + /// 북마크 버튼 클릭 + input.bookMarkButtonTapped + .bind { [weak self] in + guard let self = self else { return } + print("북마크") + } + .disposed(by: disposeBag) + + /// 메뉴 버튼 클릭 + input.menuButtonTapped + .bind { [weak self] in + guard let self = self else { return } + print("메뉴 버튼 ") + } + .disposed(by: disposeBag) + + /// 쓰기버튼 클릭 + return Output(shouldNavigateToStartToWrite: input.writeButtonTapped) + } + +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/Model/Memoirs.swift b/On_off_iOS/On_off_iOS/Memoirs/Model/Memoirs.swift new file mode 100644 index 0000000..1c3f98d --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/Model/Memoirs.swift @@ -0,0 +1,42 @@ +// +// Memoirs.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import Foundation + +struct MemoirRequest: Codable { + let date: String + let emoticonId: Int + var memoirAnswerList: [MemoirAnswer] + + struct MemoirAnswer: Codable { + let questionId: Int + let answer: String? + } +} + +struct MemoirResponse: Codable { + let isSuccess: Bool + let code: String + let message: String + let result: MemoirResult + + struct MemoirResult: Codable { + let memoirId: Int + let date: String + let emoticonUrl: String + let isBookmarked: Bool + var memoirAnswerList: [MemoirAnswerDetail] + + struct MemoirAnswerDetail: Codable { + let answerId: Int + let question: String + let summary: String + let answer: String + } + } +} + diff --git a/On_off_iOS/On_off_iOS/Memoirs/Protocol/MemoirsProtocol.swift b/On_off_iOS/On_off_iOS/Memoirs/Protocol/MemoirsProtocol.swift new file mode 100644 index 0000000..a7f9c50 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/Protocol/MemoirsProtocol.swift @@ -0,0 +1,17 @@ +// +// MemoirsProtocol.swift +// On_off_iOS +// +// Created by 박다미 on 2024/02/02. +// + +import Foundation +import RxSwift + +protocol MemoirsProtocol { + + /// 회고록 저장할때 + /// - Parameter request: 서버에 보내는 회고록 정보 + /// - Returns: MemoirResponse + func saveMemoirs(request: MemoirRequest) -> Observable +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/StartToWrite/StartToWriteViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/StartToWrite/StartToWriteViewController.swift new file mode 100644 index 0000000..41f7ffe --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/StartToWrite/StartToWriteViewController.swift @@ -0,0 +1,171 @@ +// +// StartToWriteViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import UIKit +import RxSwift +import RxCocoa + +/// 작성 시작 ViewController +final class StartToWriteViewController: UIViewController { + + /// customBackButton + private let backButton : UIBarButtonItem = { + let button = UIBarButtonItem(title: MemoirsText.getText(for: .backButton), style: .plain, target: nil, action: nil) + button.tintColor = .black + return button + }() + + /// pageControl + private lazy var pageControlImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.PageControl1.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// StartToWriteImage + private lazy var startToWriteImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.MemoirsCompleteImage.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 소개글 + private let welcomeLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .encouragement) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + label.textColor = .white + return label + }() + + /// 시작하기 버튼 + private let startButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("시작하기", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + button.titleLabel?.tintColor = .white + + return button + }() + + /// 시작하기 버튼 뷰 + private lazy var startButtonView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.OnOffMain + return view + }() + + private let viewModel: StartToWriteViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: StartToWriteViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + settingView() + addSubviews() + setupBindings() + setupStartButtonView() + } + + private func settingView(){ + view.backgroundColor = .OnOffLightMain + } + + /// 시작 버튼 속성 설정 + private func setupStartButtonView(){ + let cornerRadius = UICalculator.calculate(for: .longButtonCornerRadius, width: view.frame.width) + startButtonView.layer.cornerRadius = cornerRadius + startButtonView.layer.masksToBounds = true + } + + /// addSubviews + private func addSubviews(){ + + view.addSubview(pageControlImage) + view.addSubview(startToWriteImage) + view.addSubview(welcomeLabel) + view.addSubview(startButtonView) + startButtonView.addSubview(startButton) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints() { + + self.navigationItem.leftBarButtonItem = backButton + + pageControlImage.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.width.equalTo(view.snp.width).multipliedBy(0.25) + make.top.equalTo(view.safeAreaLayoutGuide).inset(10) + make.height.equalTo(pageControlImage.snp.width).multipliedBy(0.1) + } + + startToWriteImage.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(20) + make.height.equalTo(startToWriteImage.snp.width).multipliedBy(0.6) + make.center.equalToSuperview() + } + + startButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.width.equalTo(view.snp.width).multipliedBy(0.8) + make.height.equalTo(startButtonView.snp.width).multipliedBy(0.15) + make.centerX.equalToSuperview() + } + + startButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + welcomeLabel.snp.makeConstraints { make in + make.bottom.equalTo(startButtonView.snp.top).offset(-30) + make.centerX.equalToSuperview() + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = StartToWriteViewModel.Input(startButtonTapped: startButton.rx.tap.asObservable(), + backButtonTapped: backButton.rx.tap.asObservable()) + let output = viewModel.bind(input: input) + + output.moveToNext + .subscribe(onNext: { [weak self] _ in + self?.navigateToWriteLearned() + + }) + .disposed(by: disposeBag) + + output.moveToBack + .subscribe(onNext: { [weak self] _ in + self?.navigationController?.popViewController(animated: false) + }) + .disposed(by: disposeBag) + } + + private func navigateToWriteLearned() { + let writeLearnedViewModel = WriteLearnedViewModel() + let writeLearnedViewController = WriteLearnedViewController(viewModel: writeLearnedViewModel) + self.navigationController?.pushViewController(writeLearnedViewController, animated: false) + } + + +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/StartToWrite/StartToWriteViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/StartToWrite/StartToWriteViewModel.swift new file mode 100644 index 0000000..6370dbc --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/StartToWrite/StartToWriteViewModel.swift @@ -0,0 +1,48 @@ +// +// StartToWriteViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// StartToWriteViewModel +final class StartToWriteViewModel { + private let disposeBag = DisposeBag() + + /// Input + struct Input { + let startButtonTapped: Observable + let backButtonTapped: Observable + } + + /// Output + struct Output { + let moveToNext = PublishSubject() + let moveToBack = PublishSubject() + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = Output() + + /// 시작하기 버튼 클릭 + input.startButtonTapped + .bind(to: output.moveToNext) + .disposed(by: disposeBag) + + /// 뒤로가기 버튼 클릭 + input.backButtonTapped + .bind(to: output.moveToBack) + .disposed(by: disposeBag) + + return output + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/WriteImprovement/WriteImprovementViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/WriteImprovement/WriteImprovementViewController.swift new file mode 100644 index 0000000..3310fbd --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/WriteImprovement/WriteImprovementViewController.swift @@ -0,0 +1,244 @@ +// +// WriteImprovementViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import UIKit +import RxSwift +import RxCocoa + +final class WriteImprovementViewController: UIViewController { + + /// customBackButton + private let backButton : UIBarButtonItem = { + let button = UIBarButtonItem(title: MemoirsText.getText(for: .backButton), style: .plain, target: nil, action: nil) + button.tintColor = .black + return button + }() + + /// pageControl + private lazy var pageControlImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.PageControl3.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 윗줄 welcomeUpperLabel + private let welcomeUpperLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .praise) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 밑줄 welcomeDownLabel + private let welcomeBottomLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .selfPraisePrompt) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 회고록 작성페이지 그림 + private lazy var textpageImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.TextpageImage2.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 회고글 TextField + private let textView: UITextView = { + let textView = UITextView() + textView.textAlignment = .left + textView.font = UIFont.systemFont(ofSize: 13, weight: .regular) + textView.layer.borderWidth = 0 + textView.backgroundColor = .clear + return textView + }() + + /// 글자 수 + private let checkLenghtLabel: UILabel = { + let label = UILabel() + label.text = "(0/500)" + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 13, weight: .regular) + label.textColor = .lightGray + return label + }() + + /// 확인 버튼 + private let checkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("다음 >", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + button.titleLabel?.tintColor = .white + return button + }() + + /// 확인 버튼 뷰 + private lazy var checkButtonView: UIView = { + let view = UIView() + view.backgroundColor = .OnOffMain + view.layer.cornerRadius = 40 + view.layer.masksToBounds = true + return view + }() + + private let viewModel: WriteImprovementViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: WriteImprovementViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + settingView() + addSubviews() + setupBindings() + settingCheckButtonView() + } + + private func settingView(){ + view.backgroundColor = .OnOffLightMain + } + + /// 확인 버튼 속성 설정 + private func settingCheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .shortButtonCornerRadius, width: view.frame.width) + checkButtonView.layer.cornerRadius = cornerRadius + checkButtonView.layer.masksToBounds = true + } + + /// addSubviews + private func addSubviews() { + + view.addSubview(pageControlImage) + + view.addSubview(welcomeUpperLabel) + view.addSubview(welcomeBottomLabel) + + view.addSubview(textpageImage) + view.addSubview(textView) + + view.addSubview(checkLenghtLabel) + + view.addSubview(checkButtonView) + checkButtonView.addSubview(checkButton) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints() { + + self.navigationItem.leftBarButtonItem = backButton + + pageControlImage.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.width.equalTo(view.snp.width).multipliedBy(0.25) + make.top.equalTo(view.safeAreaLayoutGuide).inset(10) + make.height.equalTo(pageControlImage.snp.width).multipliedBy(0.1) + } + + welcomeUpperLabel.snp.makeConstraints { make in + make.top.equalTo(pageControlImage.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + welcomeBottomLabel.snp.makeConstraints { make in + make.top.equalTo(welcomeUpperLabel.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + textpageImage.snp.makeConstraints { make in + make.top.equalTo(welcomeBottomLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(textpageImage.snp.width) + } + + textView.snp.makeConstraints { make in + make.top.equalTo(textpageImage).offset(50) + make.bottom.equalTo(textpageImage).offset(-50) + make.horizontalEdges.equalTo(textpageImage).inset(30) + } + + checkLenghtLabel.snp.makeConstraints { make in + make.top.equalTo(textpageImage.snp.bottom).offset(10) + make.trailing.equalTo(textView) + } + + checkButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.trailing.equalToSuperview().inset(20) + make.width.equalTo(view.snp.width).multipliedBy(0.3) + make.height.equalTo(checkButtonView.snp.width).multipliedBy(0.5) + } + + checkButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = WriteImprovementViewModel.Input(startButtonTapped: checkButton.rx.tap.asObservable(), + textChanged: textView.rx.text.orEmpty.asObservable(), + backButtonTapped: backButton.rx.tap.asObservable()) + + let output = viewModel.bind(input: input) + + /// 글자수 출력 바인딩 + output.textLength + .map { "(\($0)/500)" } + .bind(to: checkLenghtLabel.rx.text) + .disposed(by: disposeBag) + + output.moveToNext + .subscribe(onNext: { [weak self] isSuccess in + if isSuccess { + self?.navigateToImprovement() + } else { + //실패임 + } + }) + .disposed(by: disposeBag) + + + output.moveToBack + .subscribe(onNext: { [weak self] _ in + self?.navigationController?.popViewController(animated: false) + }) + .disposed(by: disposeBag) + + } + + private func navigateToImprovement() { + let writePraisedViewModel = WritePraisedViewModel() + let writePraisedViewController = WritePraisedViewController(viewModel: writePraisedViewModel) + + self.navigationController?.pushViewController(writePraisedViewController, animated: false) + } + + // 키보드내리기 + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + textView.endEditing(true) + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/WriteImprovement/WriteImprovementViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/WriteImprovement/WriteImprovementViewModel.swift new file mode 100644 index 0000000..ac4ff6f --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/WriteImprovement/WriteImprovementViewModel.swift @@ -0,0 +1,59 @@ +// +// WriteImprovementViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// WriteImprovementViewModel +final class WriteImprovementViewModel { + private let disposeBag = DisposeBag() + + /// Input + struct Input { + let startButtonTapped: Observable + let textChanged: Observable + let backButtonTapped: Observable + } + + /// Output + struct Output { + let textLength: PublishSubject = PublishSubject() + let moveToNext = PublishSubject() + let moveToBack = PublishSubject() + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = Output() + + /// textLength + input.textChanged + .map { $0.count } + .bind(to: output.textLength) + .disposed(by: disposeBag) + + /// 완료버튼 클릭 + input.startButtonTapped + .withLatestFrom(input.textChanged) + .subscribe(onNext: { text in + let isSuccess = KeychainWrapper.saveItem(value: text, forKey: MemoirsKeyChain.MemoirsAnswer2.rawValue) + output.moveToNext.onNext(isSuccess) + }).disposed(by: disposeBag) + + /// 뒤로가기 버튼 클릭 + input.backButtonTapped + .bind(to: output.moveToBack) + .disposed(by: disposeBag) + + return output + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/WriteLearned/WriteLearnedViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/WriteLearned/WriteLearnedViewController.swift new file mode 100644 index 0000000..9db105b --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/WriteLearned/WriteLearnedViewController.swift @@ -0,0 +1,247 @@ +// +// WriteLearnedViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import UIKit +import RxSwift +import RxCocoa + +final class WriteLearnedViewController: UIViewController { + + /// customBackButton + private let backButton : UIBarButtonItem = { + let button = UIBarButtonItem(title: MemoirsText.getText(for: .backButton), style: .plain, target: nil, action: nil) + button.tintColor = .black + return button + }() + + /// pageControl + private lazy var pageControlImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.PageControl2.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 사용자 명 + private let userNameLabel: UILabel = { + let label = UILabel() + label.text = "조디" + label.numberOfLines = 0 + label.textAlignment = .center + label.textColor = .black + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// welcomeLabel + private let welcomeLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .dailyReflection) + label.numberOfLines = 0 + label.textAlignment = .center + label.textColor = .black + + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 회고록 작성페이지 그림 + private lazy var textpageImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.TextpageImage1.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 회고글 TextField + private let textView: UITextView = { + let textView = UITextView() + textView.textAlignment = .left + textView.font = UIFont.systemFont(ofSize: 13, weight: .regular) + textView.backgroundColor = UIColor.clear + textView.layer.borderWidth = 0 + textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + return textView + }() + + /// 글자 수 + private let checkLenghtLabel: UILabel = { + let label = UILabel() + label.text = "(0/500)" + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 13, weight: .regular) + label.textColor = .lightGray + return label + }() + + /// 다음 버튼 + private let checkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("다음 >", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + button.titleLabel?.tintColor = .white + return button + }() + + /// 다음 버튼 뷰 + private lazy var checkButtonView: UIView = { + let view = UIView() + view.backgroundColor = .OnOffMain + view.layer.cornerRadius = 20 + view.layer.masksToBounds = true + return view + }() + + private let viewModel: WriteLearnedViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: WriteLearnedViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + settingView() + addSubviews() + setupBindings() + settingCheckButtonView() + } + + private func settingView(){ + view.backgroundColor = .OnOffLightMain + } + + /// 확인 버튼 속성 설정 + private func settingCheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .shortButtonCornerRadius, width: view.frame.width) + checkButtonView.layer.cornerRadius = cornerRadius + checkButtonView.layer.masksToBounds = true + } + + /// addSubviews + private func addSubviews() { + + view.addSubview(pageControlImage) + + view.addSubview(userNameLabel) + view.addSubview(welcomeLabel) + + view.addSubview(textpageImage) + view.addSubview(textView) + view.addSubview(checkLenghtLabel) + + view.addSubview(checkButtonView) + checkButtonView.addSubview(checkButton) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints() { + + self.navigationItem.leftBarButtonItem = backButton + + pageControlImage.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.width.equalTo(view.snp.width).multipliedBy(0.25) + make.top.equalTo(view.safeAreaLayoutGuide).inset(10) + make.height.equalTo(pageControlImage.snp.width).multipliedBy(0.1) + } + + userNameLabel.snp.makeConstraints { make in + make.top.equalTo(pageControlImage.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + welcomeLabel.snp.makeConstraints { make in + make.top.equalTo(userNameLabel.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + textpageImage.snp.makeConstraints { make in + make.top.equalTo(welcomeLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(textpageImage.snp.width) + } + + textView.snp.makeConstraints { make in + make.top.equalTo(textpageImage).offset(50) + make.bottom.equalTo(textpageImage).offset(-50) + make.horizontalEdges.equalTo(textpageImage).inset(30) + } + + checkLenghtLabel.snp.makeConstraints { make in + make.top.equalTo(textpageImage.snp.bottom).offset(10) + make.trailing.equalTo(textView) + } + + checkButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.trailing.equalToSuperview().inset(20) + make.width.equalTo(view.snp.width).multipliedBy(0.3) + make.height.equalTo(checkButtonView.snp.width).multipliedBy(0.5) + } + + checkButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = WriteLearnedViewModel.Input( + startButtonTapped: checkButton.rx.tap.asObservable(), + textChanged: textView.rx.text.orEmpty.asObservable(), + backButtonTapped: backButton.rx.tap.asObservable() + ) + + let output = viewModel.bind(input: input) + + /// 글자수 출력 바인딩 + output.textLength + .map { "(\($0)/500)" } + .bind(to: checkLenghtLabel.rx.text) + .disposed(by: disposeBag) + + output.saveResult + .subscribe(onNext: { [weak self] isSuccess in + if isSuccess { + self?.navigateToImprovement() + } else { + //실패임 + } + }) + .disposed(by: disposeBag) + + + output.moveToBack + .subscribe(onNext: { [weak self] _ in + self?.navigationController?.popViewController(animated: false) + }) + .disposed(by: disposeBag) + } + + private func navigateToImprovement() { + let writeImprovementViewModel = WriteImprovementViewModel() + let writeImprovementViewController = WriteImprovementViewController(viewModel: writeImprovementViewModel) + self.navigationController?.pushViewController(writeImprovementViewController, animated: false) + } + + // 키보드내리기 + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + textView.endEditing(true) + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/WriteLearned/WriteLearnedViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/WriteLearned/WriteLearnedViewModel.swift new file mode 100644 index 0000000..c227bd7 --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/WriteLearned/WriteLearnedViewModel.swift @@ -0,0 +1,57 @@ +// +// WriteLearnedViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// WriteLearnedViewModel +final class WriteLearnedViewModel { + private let disposeBag = DisposeBag() + /// Input + struct Input { + let startButtonTapped: Observable + let textChanged: Observable + let backButtonTapped: Observable + + } + + struct Output { + let textLength = PublishSubject() + let saveResult = PublishSubject() // 저장 성공 여부를 나타내는 PublishSubject + let moveToBack = PublishSubject() + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = Output() + + /// textLength + input.textChanged + .map { $0.count } + .bind(to: output.textLength) + .disposed(by: disposeBag) + + input.startButtonTapped + .withLatestFrom(input.textChanged) + .subscribe(onNext: { text in + let isSuccess = KeychainWrapper.saveItem(value: text, forKey: MemoirsKeyChain.MemoirsAnswer1.rawValue) + output.saveResult.onNext(isSuccess) + }).disposed(by: disposeBag) + + /// 뒤로가기 버튼 클릭 + input.backButtonTapped + .bind(to: output.moveToBack) + .disposed(by: disposeBag) + + return output + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/WritePraised/WritePraisedViewController.swift b/On_off_iOS/On_off_iOS/Memoirs/WritePraised/WritePraisedViewController.swift new file mode 100644 index 0000000..5d1ffec --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/WritePraised/WritePraisedViewController.swift @@ -0,0 +1,239 @@ +// +// WritePraisedViewController.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import UIKit +import RxSwift +import RxCocoa + +/// WritePraisedViewController +final class WritePraisedViewController: UIViewController { + + /// customBackButton + private let backButton : UIBarButtonItem = { + let button = UIBarButtonItem(title: MemoirsText.getText(for: .backButton), style: .plain, target: nil, action: nil) + button.tintColor = .black + return button + }() + + /// pageControl + private lazy var pageControlImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.PageControl4.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 가장 윗줄 label + private let welcomeUpperLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .difficultyPrompt) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// welcomeLabel + private let welcomeBottomLabel: UILabel = { + let label = UILabel() + label.text = MemoirsText.getText(for: .improvementPrompt) + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20, weight: .bold) + return label + }() + + /// 회고록 작성페이지 그림 + private lazy var textpageImage: UIImageView = { + let imageView = UIImageView(image: UIImage(named: MemoirsImage.TextpageImage3.rawValue)) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + /// 회고글 TextField + private let textView: UITextView = { + let textView = UITextView() + textView.textAlignment = .left + textView.font = UIFont.systemFont(ofSize: 13, weight: .regular) + textView.backgroundColor = UIColor.clear + textView.layer.borderWidth = 0 + textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + return textView + }() + + /// 글자 수 + private let checkLenghtLabel: UILabel = { + let label = UILabel() + label.text = "(0/500)" + label.numberOfLines = 0 + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 13, weight: .regular) + label.textColor = .lightGray + return label + }() + + /// 확인 버튼 + private let checkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("다음 >", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + button.titleLabel?.tintColor = .white + return button + }() + + /// 확인 버튼 뷰 + private lazy var checkButtonView: UIView = { + let view = UIView() + view.backgroundColor = .OnOffMain + return view + }() + + private let viewModel: WritePraisedViewModel + private let disposeBag = DisposeBag() + + // MARK: - Init + init(viewModel: WritePraisedViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + settingView() + addSubviews() + settingCheckButtonView() + setupBindings() + } + + private func settingView(){ + view.backgroundColor = .OnOffLightMain + } + + /// 확인 버튼 속성 설정 + private func settingCheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .shortButtonCornerRadius, width: view.frame.width) + checkButtonView.layer.cornerRadius = cornerRadius + checkButtonView.layer.masksToBounds = true + } + + /// addSubviews + private func addSubviews() { + + view.addSubview(pageControlImage) + view.addSubview(welcomeUpperLabel) + view.addSubview(welcomeBottomLabel) + + view.addSubview(textpageImage) + view.addSubview(textView) + + view.addSubview(checkLenghtLabel) + view.addSubview(checkButtonView) + checkButtonView.addSubview(checkButton) + + configureConstraints() + } + + /// configureConstraints + private func configureConstraints() { + + self.navigationItem.leftBarButtonItem = backButton + + pageControlImage.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.width.equalTo(view.snp.width).multipliedBy(0.25) + make.top.equalTo(view.safeAreaLayoutGuide).inset(10) + make.height.equalTo(pageControlImage.snp.width).multipliedBy(0.1) + } + + welcomeUpperLabel.snp.makeConstraints { make in + make.top.equalTo(pageControlImage.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + welcomeBottomLabel.snp.makeConstraints { make in + make.top.equalTo(welcomeUpperLabel.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + textpageImage.snp.makeConstraints { make in + make.top.equalTo(welcomeBottomLabel.snp.bottom).offset(10) + make.leading.trailing.equalToSuperview().inset(10) + make.height.equalTo(textpageImage.snp.width) + } + + textView.snp.makeConstraints { make in + make.top.equalTo(textpageImage).offset(50) + make.bottom.equalTo(textpageImage).offset(-50) + make.horizontalEdges.equalTo(textpageImage).inset(30) + } + + checkLenghtLabel.snp.makeConstraints { make in + make.top.equalTo(textpageImage.snp.bottom).offset(10) + make.trailing.equalTo(textView) + } + + checkButtonView.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(50) + make.trailing.equalToSuperview().inset(20) + make.width.equalTo(view.snp.width).multipliedBy(0.3) + make.height.equalTo(checkButtonView.snp.width).multipliedBy(0.5) + } + + checkButton.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + /// 뷰모델과 setupBindings + private func setupBindings() { + let input = WritePraisedViewModel.Input(startButtonTapped: checkButton.rx.tap.asObservable(), + textChanged: textView.rx.text.orEmpty.asObservable(), backButtonTapped: backButton.rx.tap.asObservable()) + + let output = viewModel.bind(input: input) + + /// 글자수 출력 바인딩 + output.textLength + .map { "(\($0)/500)" } + .bind(to: checkLenghtLabel.rx.text) + .disposed(by: disposeBag) + + output.moveToNext + .subscribe(onNext: { [weak self] isSuccess in + if isSuccess { + self?.navigateToExpressdIcon() + } else { + //실패임 + } + }) + .disposed(by: disposeBag) + + + output.moveToBack + .subscribe(onNext: { [weak self] _ in + self?.navigationController?.popViewController(animated: false) + }) + .disposed(by: disposeBag) + } + + private func navigateToExpressdIcon() { + let expressedIconViewModel = ExpressedIconViewModel() + let expressedIconViewController = ExpressedIconViewController(viewModel: expressedIconViewModel) + self.navigationController?.pushViewController(expressedIconViewController, animated: false) + } + + // 키보드내리기 + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + textView.endEditing(true) + } +} diff --git a/On_off_iOS/On_off_iOS/Memoirs/WritePraised/WritePraisedViewModel.swift b/On_off_iOS/On_off_iOS/Memoirs/WritePraised/WritePraisedViewModel.swift new file mode 100644 index 0000000..9bf5dfe --- /dev/null +++ b/On_off_iOS/On_off_iOS/Memoirs/WritePraised/WritePraisedViewModel.swift @@ -0,0 +1,60 @@ +// +// WritePraisedViewModel.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/20. +// + +import RxCocoa +import RxRelay +import RxSwift +import UIKit + +/// WritePraisedViewModel +final class WritePraisedViewModel { + private let disposeBag = DisposeBag() + + /// Input + struct Input { + let startButtonTapped: Observable + let textChanged: Observable + let backButtonTapped: Observable + } + + /// Output + struct Output { + let textLength: PublishSubject = PublishSubject() + let moveToNext = PublishSubject() + let moveToBack = PublishSubject() + } + + /// binding Input + /// - Parameter + /// - input: Input 구조체 + /// - Returns: Output 구조체 + func bind(input: Input) -> Output { + let output = Output() + + /// textLength + input.textChanged + .map { $0.count } + .bind(to: output.textLength) + .disposed(by: disposeBag) + + /// 완료버튼 클릭 + input.startButtonTapped + .withLatestFrom(input.textChanged) + .subscribe(onNext: { text in + let isSuccess = KeychainWrapper.saveItem(value: text, forKey: MemoirsKeyChain.MemoirsAnswer3.rawValue) + output.moveToNext.onNext(isSuccess) + }).disposed(by: disposeBag) + + /// 뒤로가기 버튼 클릭 + input.backButtonTapped + .bind(to: output.moveToBack) + .disposed(by: disposeBag) + + return output + } + /// ExpressedIconViewModel +} diff --git a/On_off_iOS/On_off_iOS/OnBoarding/View/CustomPageControl.swift b/On_off_iOS/On_off_iOS/OnBoarding/View/CustomPageControl.swift index 8abea95..5b6f085 100644 --- a/On_off_iOS/On_off_iOS/OnBoarding/View/CustomPageControl.swift +++ b/On_off_iOS/On_off_iOS/OnBoarding/View/CustomPageControl.swift @@ -40,7 +40,7 @@ final class CustomPageControl: UIView { private func updateDotStyles() { for (index, dot) in dotViews.enumerated() { if index == currentPage { - dot.backgroundColor = .gray + dot.backgroundColor = UIColor.OnOffMain dot.frame.size = activeDotSize dot.layer.cornerRadius = activeDotSize.height / 2 continue diff --git a/On_off_iOS/On_off_iOS/OnBoarding/View/OnBoardingViewController.swift b/On_off_iOS/On_off_iOS/OnBoarding/View/OnBoardingViewController.swift index fd3c8e7..2f324d7 100644 --- a/On_off_iOS/On_off_iOS/OnBoarding/View/OnBoardingViewController.swift +++ b/On_off_iOS/On_off_iOS/OnBoarding/View/OnBoardingViewController.swift @@ -23,8 +23,8 @@ final class OnBoardingViewController : UIViewController { scrollView.delegate = self return scrollView }() - let contentView = UIView() - + private let contentView = UIView() + /// 현재 페이지 상태를 관리 private var currentPage = BehaviorRelay(value: 0) private let totalPages = 3 @@ -38,9 +38,10 @@ final class OnBoardingViewController : UIViewController { /// 다음, 건너뛰기 버튼아래 뷰 private lazy var buttonView: UIView = { let view = UIView() - view.backgroundColor = .blue + view.backgroundColor = UIColor.OnOffMain return view }() + /// 다음버튼 private let nextButton : UIButton = { let button = UIButton() @@ -56,8 +57,8 @@ final class OnBoardingViewController : UIViewController { }() private let disposeBag = DisposeBag() - private var viewModel: OnBoardingViewModel - + private let viewModel: OnBoardingViewModel + // MARK: - Init init(viewModel: OnBoardingViewModel) { self.viewModel = viewModel @@ -75,20 +76,22 @@ final class OnBoardingViewController : UIViewController { setupUI() addSubViews() setupBindings() - + } + private func setupUI(){ view.backgroundColor = .white navigationController?.navigationBar.isHidden = true - } + /// 온보딩 뷰들을 설정 private func setupOnboardingViews(in contentView: UIView) { - let onboardingData: [OnboardingItem] = [ - OnboardingItem(imageName: "온보딩1", text: "퇴근길 회고하며 이제\n일에서 완전히 로그아웃하세요"), - OnboardingItem(imageName: "온보딩2", text: "왜 자꾸 실수할까?\n쌓인 회고들이 나를 성장시킬거예요"), - OnboardingItem(imageName: "온보딩3", text: "on & off로\n일과 삶의 밸런스를 관리해요!") + let onboardingData: [(imageName: String, text: NSAttributedString)] = [ + (imageName: "온보딩1", text: createAttributedText(for: "퇴근길 회고하며 이제\n일에서 완전히 로그아웃하세요", highlightWords: [("회고", UIColor.OnOffMain, UIFont.boldSystemFont(ofSize: 22)), ("로그아웃", UIColor.OnOffMain, UIFont.boldSystemFont(ofSize: 22))])), + (imageName: "온보딩2", text: createAttributedText(for: "왜 자꾸 실수할까?\n쌓인 회고들이 나를 성장시킬거예요", highlightWords: [("성장", UIColor.OnOffMain,UIFont.boldSystemFont(ofSize: 22))])), + (imageName: "온보딩3", text: createAttributedText(for: "ON&OFF로\n일과 삶의 밸런스를 관리해요!", highlightWords: [("ON&OFF", UIColor.OnOffMain, UIFont.boldSystemFont(ofSize: 22)), ("밸런스", UIColor.OnOffMain, UIFont.boldSystemFont(ofSize: 22))])) ] + /// 이전 뷰 추적함 var previousView: UIView? @@ -127,10 +130,10 @@ final class OnBoardingViewController : UIViewController { view.addSubview(buttonView) buttonView.addSubview(nextButton) buttonView.addSubview(jumpButton) - + configureConstraints() setupOnboardingViews(in: contentView) - + } /// configureConstraints @@ -157,10 +160,12 @@ final class OnBoardingViewController : UIViewController { make.leading.trailing.bottom.equalToSuperview() make.height.equalTo(buttonView.snp.width).multipliedBy(0.2) } + nextButton.snp.makeConstraints { make in make.trailing.equalToSuperview().inset(17) make.bottom.equalToSuperview().inset(30) } + jumpButton.snp.makeConstraints { make in make.leading.equalToSuperview().inset(17) make.bottom.equalToSuperview().inset(30) @@ -178,23 +183,30 @@ final class OnBoardingViewController : UIViewController { .map { _ in Void() } let input = OnBoardingViewModel.Input( - startButtonTapped: startButtonTapOnLastPage, + startButtonTapped: startButtonTapOnLastPage, jumpButtonTapped: jumpButton.rx.tap.asObservable() ) - let _ = viewModel.bind(input: input) + let output = viewModel.bind(input: input) + + // 로그인 화면으로 이동하는 이벤트 구독 + output.moveToLogin + .subscribe(onNext: { [weak self] _ in + self?.moveToLogin() + }) + .disposed(by: disposeBag) // 나머지 페이지에서 버튼이 눌렸을 때의 동작 nextButton.rx.tap .withLatestFrom(currentPage.asObservable()) .filter { [weak self] page in guard let self = self else { return false } - return page < self.totalPages - 1 + return page < totalPages - 1 } .subscribe(onNext: { [weak self] _ in guard let self = self else { return } - let nextPage = (self.currentPage.value + 1) % self.totalPages - self.currentPage.accept(nextPage) + let nextPage = (self.currentPage.value + 1) % totalPages + currentPage.accept(nextPage) }).disposed(by: disposeBag) // 현재 페이지 변경 감지 @@ -202,23 +214,22 @@ final class OnBoardingViewController : UIViewController { guard let self = self else { return } let isLastPage = page == totalPages - 1 - self.nextButton.snp.remakeConstraints { make in + nextButton.snp.remakeConstraints { make in make.trailing.equalToSuperview().inset(17) make.bottom.equalToSuperview().inset(30) } - self.jumpButton.isHidden = false - - + self.jumpButton.isHidden = false + // 마지막 페이지인 경우 - if isLastPage { - self.nextButton.snp.remakeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().inset(30) - } - self.jumpButton.isHidden = true - } + if isLastPage { + nextButton.snp.remakeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().inset(30) + } + self.jumpButton.isHidden = true + } - let buttonTitle = page == self.totalPages - 1 ? "시작하기" : "다음" + let buttonTitle = page == self.totalPages - 1 ? "온앤오프 시작하기" : "다음" self.nextButton.setTitle(buttonTitle, for: .normal) let xOffset = CGFloat(page) * self.view.frame.width @@ -230,6 +241,14 @@ final class OnBoardingViewController : UIViewController { }).disposed(by: disposeBag) } + + /// 로그인 화면으로 이동 + private func moveToLogin() { + let loginService = LoginService() + let loginViewModel = LoginViewModel(loginService: loginService) + let vc = LoginViewController(viewModel: loginViewModel) + self.navigationController?.pushViewController(vc, animated: true) + } } // MARK: Extension - UIScrollViewDelegate diff --git a/On_off_iOS/On_off_iOS/OnBoarding/View/OnboardingCustomView.swift b/On_off_iOS/On_off_iOS/OnBoarding/View/OnboardingCustomView.swift index 1a2f4d5..3245bed 100644 --- a/On_off_iOS/On_off_iOS/OnBoarding/View/OnboardingCustomView.swift +++ b/On_off_iOS/On_off_iOS/OnBoarding/View/OnboardingCustomView.swift @@ -54,9 +54,9 @@ final class OnboardingCustomView: UIView { } /// configure - func configure(imageName: String, text: String) { + func configure(imageName: String, text: NSAttributedString) { onboardingImageView.image = UIImage(named: imageName) - titleLabel.text = text + titleLabel.attributedText = text } } diff --git a/On_off_iOS/On_off_iOS/OnBoarding/ViewModel/OnBoardingViewModel.swift b/On_off_iOS/On_off_iOS/OnBoarding/ViewModel/OnBoardingViewModel.swift index a42e449..8e585f9 100644 --- a/On_off_iOS/On_off_iOS/OnBoarding/ViewModel/OnBoardingViewModel.swift +++ b/On_off_iOS/On_off_iOS/OnBoarding/ViewModel/OnBoardingViewModel.swift @@ -12,39 +12,35 @@ import UIKit final class OnBoardingViewModel { private let disposeBag = DisposeBag() - var navigationController: UINavigationController /// Input struct Input { let startButtonTapped: Observable let jumpButtonTapped: Observable } - - // MARK: - Init - init(navigationController: UINavigationController) { - self.navigationController = navigationController + + /// Output + struct Output { + let moveToLogin = PublishSubject() } /// bind /// - Parameter input:startButtonTapped, jumpButtonTapped - func bind(input: Input) { + func bind(input: Input) -> Output { + let output = Output() + input.startButtonTapped - .bind { [weak self] in - self?.moveToLogin() + .bind { _ in + output.moveToLogin.onNext(()) } .disposed(by: disposeBag) input.jumpButtonTapped - .bind { [weak self] in - self?.moveToLogin() + .bind { _ in + output.moveToLogin.onNext(()) } .disposed(by: disposeBag) - } - - /// 로그인 화면으로 이동 - private func moveToLogin() { - let loginViewModel = LoginViewModel(navigationController: navigationController) - let vc = LoginViewController(viewModel: loginViewModel) - navigationController.pushViewController(vc, animated: true) + + return output } } diff --git a/On_off_iOS/On_off_iOS/On_off_iOS.entitlements b/On_off_iOS/On_off_iOS/On_off_iOS.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/On_off_iOS/On_off_iOS/On_off_iOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/On_off_iOS/On_off_iOS/ProfileSetting/View/ProfileSettingViewController.swift b/On_off_iOS/On_off_iOS/ProfileSetting/View/ProfileSettingViewController.swift deleted file mode 100644 index e34caac..0000000 --- a/On_off_iOS/On_off_iOS/ProfileSetting/View/ProfileSettingViewController.swift +++ /dev/null @@ -1,247 +0,0 @@ -// -// ProfileSettingViewController.swift -// On_off_iOS -// -// Created by 박다미 on 2024/01/01. -// - -import UIKit -import RxSwift -import RxCocoa - -/// 닉네임 설정 -final class ProfileSettingViewController: UIViewController { - - /// 업무분야 - private lazy var fieldOfWork: UILabel = { - let label = UILabel() - label.text = "업무 분야" - label.textColor = .black - label.font = .systemFont(ofSize: 20, weight: .bold) - return label - }() - - /// 업무분야 - 텍스트 필드 - private lazy var fieldOfWorkTextField: UITextField = { - let field = UITextField() - field.attributedPlaceholder = NSAttributedString(string: "예시) 커머스, 여행, 소셜, AI, 제조업 등", - attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray]) - field.textAlignment = .left - field.font = UIFont.systemFont(ofSize: 11, weight: .regular) - field.backgroundColor = UIColor.clear - field.layer.borderWidth = 0 - return field - }() - - /// 업무분야 - 밑줄 - private lazy var fieldOfWorkLine: UIView = { - let lineView = UIView() - lineView.backgroundColor = .black - return lineView - }() - - /// 직업 - private lazy var job: UILabel = { - let label = UILabel() - label.text = "직업" - label.textColor = .black - label.font = .systemFont(ofSize: 20, weight: .bold) - return label - }() - /// 직업 - 텍스트 필드 - private lazy var jobTextField: UITextField = { - let field = UITextField() - field.attributedPlaceholder = NSAttributedString(string: "예시) 서비스 기획자, UX 디자이너, 개발자 등", - attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray]) - field.textAlignment = .left - field.backgroundColor = UIColor.clear - field.layer.cornerRadius = 10 - field.layer.borderWidth = 1 - field.font = UIFont.systemFont(ofSize: 13, weight: .regular) - field.layer.borderColor = UIColor.clear.cgColor - field.textColor = .black - - return field - }() - - /// 직업 - 밑줄 - private lazy var jobLine : UIView = { - let lineView = UIView() - lineView.backgroundColor = .black - return lineView - }() - - /// 연차 - private lazy var annual: UILabel = { - let label = UILabel() - label.text = "연차" - label.textColor = .black - label.font = .systemFont(ofSize: 20, weight: .bold) - return label - }() - - /// 연차 - 텍스트 필드 - private lazy var annualTextField: UITextField = { - let field = UITextField() - field.attributedPlaceholder = NSAttributedString(string: "예시) 인턴, 신입, 1년, 5년 이상, 시니어 등 ", - attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray]) - field.textAlignment = .left - field.backgroundColor = UIColor.clear - field.font = UIFont.systemFont(ofSize: 13, weight: .regular) - field.layer.borderWidth = 0 - return field - }() - - /// 연차 - 밑줄 - private lazy var annualLine: UIView = { - let lineView = UIView() - lineView.backgroundColor = .black - return lineView - }() - - private let nickNameExplainLabel: UILabel = { - let label = UILabel() - label.text = " 업무 분야, 직업, 연차는 추후에 ‘마이 페이지’에서 수정할 수 있어요. " - label.numberOfLines = 1 - label.textAlignment = .center - label.font = UIFont.systemFont(ofSize: 10, weight: .regular) - return label - }() - - private let checkButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("확인", for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 18) - return button - }() - private lazy var checkButtonView: UIView = { - let view = UIView() - view.backgroundColor = .blue - return view - }() - - private var viewModel: ProfileSettingViewModel - private let disposeBag = DisposeBag() - - // MARK: - Init - init(viewModel: ProfileSettingViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - ViewDidLoad - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - addSubviews() - setupBindings() - } - - // 키보드내리기 - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - fieldOfWorkTextField.endEditing(true) - jobTextField.endEditing(true) - annualTextField.endEditing(true) - } - - /// addSubviews - private func addSubviews(){ - - view.addSubview(fieldOfWork) - view.addSubview(fieldOfWorkTextField) - view.addSubview(fieldOfWorkLine) - - view.addSubview(job) - view.addSubview(jobTextField) - view.addSubview(jobLine) - - view.addSubview(annual) - view.addSubview(annualTextField) - view.addSubview(annualLine) - - view.addSubview(nickNameExplainLabel) - view.addSubview(checkButtonView) - checkButtonView.addSubview(checkButton) - configureConstraints() - } - - /// configureConstraints - private func configureConstraints(){ - - fieldOfWork.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide).offset(100) - make.leading.equalToSuperview().offset(10) - } - fieldOfWorkTextField.snp.makeConstraints { make in - make.top.equalTo(fieldOfWork.snp.bottom).offset(10) - make.leading.equalToSuperview().inset(10) - make.width.equalToSuperview().multipliedBy(0.8) - } - fieldOfWorkLine.snp.makeConstraints { make in - make.top.equalTo(fieldOfWorkTextField.snp.bottom).offset(8) - make.leading.trailing.equalToSuperview().inset(10) - make.height.equalTo(1) - } - - /// 직업 - job.snp.makeConstraints { make in - make.top.equalTo(fieldOfWorkLine.snp.bottom).offset(50) - make.leading.equalToSuperview().offset(10) - } - jobTextField.snp.makeConstraints { make in - make.top.equalTo(job.snp.bottom).offset(10) - make.leading.equalToSuperview().inset(10) - make.width.equalToSuperview().multipliedBy(0.8) - } - jobLine.snp.makeConstraints { make in - make.top.equalTo(jobTextField.snp.bottom).offset(8) - make.leading.trailing.equalToSuperview().inset(10) - make.height.equalTo(1) - } - - /// 연차 - annual.snp.makeConstraints { make in - make.top.equalTo(jobLine.snp.bottom).offset(50) - make.leading.equalToSuperview().offset(10) - } - annualTextField.snp.makeConstraints { make in - make.top.equalTo(annual.snp.bottom).offset(10) - make.leading.equalToSuperview().inset(10) - make.width.equalToSuperview().multipliedBy(0.8) - } - annualLine.snp.makeConstraints { make in - make.top.equalTo(annualTextField.snp.bottom).offset(8) - make.leading.trailing.equalToSuperview().inset(10) - make.height.equalTo(1) - - } - nickNameExplainLabel.snp.makeConstraints { make in - make.bottom.equalTo(checkButton.snp.top).offset(-20) - make.leading.trailing.equalToSuperview().inset(10) - } - - checkButtonView.snp.makeConstraints { make in - make.bottom.equalToSuperview().inset(50) - make.height.equalTo(checkButtonView.snp.width).multipliedBy(0.15) - make.leading.trailing.equalToSuperview().inset(17) - - checkButton.snp.makeConstraints { make in - make.center.equalToSuperview() - } - } - } - - /// 뷰모델과 setupBindings - private func setupBindings() { - let input = ProfileSettingViewModel.Input(startButtonTapped: checkButton.rx.tap.asObservable()) - - viewModel.bind(input: input) - - } -} diff --git a/On_off_iOS/On_off_iOS/ProfileSetting/ViewModel/ProfileSettingViewModel.swift b/On_off_iOS/On_off_iOS/ProfileSetting/ViewModel/ProfileSettingViewModel.swift deleted file mode 100644 index 7624d90..0000000 --- a/On_off_iOS/On_off_iOS/ProfileSetting/ViewModel/ProfileSettingViewModel.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ProfileSettingViewModel.swift -// On_off_iOS -// -// Created by 박다미 on 2024/01/01. -// - -import RxCocoa -import RxRelay -import RxSwift -import UIKit - -/// ProfileSettingViewModel -final class ProfileSettingViewModel { - private let disposeBag = DisposeBag() - private var navigationController: UINavigationController - - /// Input - struct Input { - let startButtonTapped: Observable - } - - // MARK: - Init - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - /// binding Input - /// - Parameter - /// - input: Input 구조체 - func bind(input: Input) { - - /// 시작 버튼 클릭 - input.startButtonTapped - .bind { [weak self] in - self?.moveToSelectTime() - } - .disposed(by: disposeBag) - } - /// 프로필설정으로 이동 - private func moveToSelectTime() { - let selectTimeViewModel = SelectTimeViewModel(navigationController: navigationController) - let vc = SelectTimeViewController(viewModel: selectTimeViewModel) - navigationController.pushViewController(vc, animated: true) - } -} diff --git a/On_off_iOS/On_off_iOS/SelectTime/View/DayButton.swift b/On_off_iOS/On_off_iOS/SelectTime/View/DayButton.swift index f75135e..ee1883b 100644 --- a/On_off_iOS/On_off_iOS/SelectTime/View/DayButton.swift +++ b/On_off_iOS/On_off_iOS/SelectTime/View/DayButton.swift @@ -34,7 +34,7 @@ final class DayButton: UIButton { private func setupUI() { self.layer.cornerRadius = 5 self.backgroundColor = UIColor.lightGray - self.setTitleColor(.white, for: .normal) + self.setTitleColor(.black, for: .normal) } /// 버튼 클릭 bind @@ -50,6 +50,8 @@ final class DayButton: UIButton { /// 버튼 클릭시 토글적용 업데이트 private func updateUI() { - self.backgroundColor = dayModel.isChecked ? UIColor.purple : UIColor.lightGray + self.backgroundColor = dayModel.isChecked ? .OnOffMain : .lightGray + self.setTitleColor(dayModel.isChecked ? .white : .black, for: .normal) + } } diff --git a/On_off_iOS/On_off_iOS/SelectTime/View/SelectTimeViewController.swift b/On_off_iOS/On_off_iOS/SelectTime/View/SelectTimeViewController.swift index a3944f0..f771613 100644 --- a/On_off_iOS/On_off_iOS/SelectTime/View/SelectTimeViewController.swift +++ b/On_off_iOS/On_off_iOS/SelectTime/View/SelectTimeViewController.swift @@ -60,7 +60,7 @@ final class SelectTimeViewController : UIViewController { let label = UILabel() label.text = "Status" label.textColor = .lightGray - label.font = .systemFont(ofSize: 15, weight: .regular) + label.font = .systemFont(ofSize: 12, weight: .regular) return label }() @@ -85,14 +85,15 @@ final class SelectTimeViewController : UIViewController { /// 확인버튼 private let checkButton: UIButton = { let button = UIButton(type: .system) - button.setTitle("확인", for: .normal) + button.setTitle("알람 설정 저장하기", for: .normal) + button.titleLabel?.tintColor = .white button.titleLabel?.font = UIFont.systemFont(ofSize: 18) return button }() private lazy var checkButtonView: UIView = { let view = UIView() - view.backgroundColor = .blue + view.backgroundColor = .OnOffMain return view }() @@ -113,6 +114,7 @@ final class SelectTimeViewController : UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white + setupCheckButtonView() setupDayButtons() addSubiews() setupBindings() @@ -147,31 +149,31 @@ final class SelectTimeViewController : UIViewController { /// configureConstraint private func configureConstraint() { clockImage.snp.makeConstraints { make in - make.top.equalToSuperview().inset(20) - make.leading.trailing.equalToSuperview().inset(50) + make.top.equalToSuperview().inset(50) + make.leading.trailing.equalToSuperview().inset(80) make.height.equalTo(clockImage.snp.width) } titleLabel.snp.makeConstraints { make in - make.top.equalTo(clockImage.snp.bottom).offset(10) + make.top.equalTo(clockImage.snp.bottom).offset(15) make.centerX.equalToSuperview() } subTitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(10) + make.top.equalTo(titleLabel.snp.bottom).offset(15) make.centerX.equalToSuperview() } stackView.snp.makeConstraints { make in - make.top.equalTo(subTitleLabel.snp.bottom).offset(10) + make.top.equalTo(subTitleLabel.snp.bottom).offset(15) make.centerX.equalToSuperview() make.leading.trailing.equalToSuperview().inset(10) } selectedTimeButton.snp.makeConstraints { make in - make.top.equalTo(stackView.snp.bottom).offset(30) + make.top.equalTo(stackView.snp.bottom).offset(35) make.centerX.equalToSuperview() - make.height.equalTo(selectedTimeButton.snp.width).multipliedBy(0.15) + make.height.equalTo(selectedTimeButton.snp.width).multipliedBy(0.13) make.leading.trailing.equalToSuperview().inset(10) } @@ -195,13 +197,30 @@ final class SelectTimeViewController : UIViewController { } /// DatePicker 값 바인딩 private func setupBindings() { + let input = SelectTimeViewModel.Input(startButtonTapped: checkButton.rx.tap.asObservable()) + let output = viewModel.bind(input: input) + selectedTimeButton.rx.tap .bind { [weak self] in self?.showTimePicker() } .disposed(by: disposeBag) + + + output.moveToNext + .subscribe(onNext: { [weak self] in + self?.moveToMain() + }) + .disposed(by: disposeBag) + } + /// 확인 버튼 속성 설정 + private func setupCheckButtonView(){ + let cornerRadius = UICalculator.calculate(for: .longButtonCornerRadius, width: view.frame.width) + checkButtonView.layer.cornerRadius = cornerRadius + checkButtonView.layer.masksToBounds = true + } /// timePicker 창 보기 private func showTimePicker() { let alertController = UIAlertController(title: "시간 선택", message: nil, preferredStyle: .actionSheet) @@ -241,7 +260,6 @@ final class SelectTimeViewController : UIViewController { }) alertController.addAction(selectAction) - present(alertController, animated: true, completion: nil) } @@ -253,4 +271,11 @@ final class SelectTimeViewController : UIViewController { formatter.pmSymbol = "오후" // 오후 selectedTimeButton.setTitle(formatter.string(from: date), for: .normal) } + + /// 메인 화면으로 이동 + private func moveToMain() { + let bookmarkViewModel = BookmarkViewModel() + let vc = BookmarkViewController(viewModel: bookmarkViewModel) + self.navigationController?.pushViewController(vc, animated: true) + } } diff --git a/On_off_iOS/On_off_iOS/SelectTime/ViewModel/SelectTimeViewModel.swift b/On_off_iOS/On_off_iOS/SelectTime/ViewModel/SelectTimeViewModel.swift index 9954998..c89405f 100644 --- a/On_off_iOS/On_off_iOS/SelectTime/ViewModel/SelectTimeViewModel.swift +++ b/On_off_iOS/On_off_iOS/SelectTime/ViewModel/SelectTimeViewModel.swift @@ -13,12 +13,10 @@ import UIKit /// SelectTimeViewModel final class SelectTimeViewModel { private let disposeBag = DisposeBag() - var navigationController: UINavigationController /// Input struct Input { let startButtonTapped: Observable - let nickNameTextChanged: Observable } /// Output @@ -26,12 +24,9 @@ final class SelectTimeViewModel { let nickNameFilteringRelay: BehaviorRelay = BehaviorRelay(value: "") let nickNameLength: PublishSubject = PublishSubject() let isCheckButtonEnabled: BehaviorRelay = BehaviorRelay(value: true) + let moveToNext = PublishSubject() } - - // MARK: - Init - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } + /// binding Input /// - Parameter @@ -39,6 +34,11 @@ final class SelectTimeViewModel { /// - Returns: Output 구조체 func bind(input: Input) -> Output { let output = Output() + + input.startButtonTapped + .bind(to:output.moveToNext) + .disposed(by: disposeBag) + return output } } diff --git a/On_off_iOS/On_off_iOS/UICalculate+Ex.swift b/On_off_iOS/On_off_iOS/UICalculate+Ex.swift new file mode 100644 index 0000000..e0b66e9 --- /dev/null +++ b/On_off_iOS/On_off_iOS/UICalculate+Ex.swift @@ -0,0 +1,29 @@ +// +// UICalculate+Ex.swift +// On_off_iOS +// +// Created by 박다미 on 2024/01/28. +// + +import Foundation + +///UI고정된 계산값 +struct UICalculator { + + enum CalculationType { + case shortButtonCornerRadius + case longButtonCornerRadius + } + + static func calculate(for type: CalculationType, width: CGFloat) -> CGFloat { + switch type { + case .shortButtonCornerRadius: + return width * 0.8 * 0.15 * 0.5 + + case .longButtonCornerRadius: + return width * 0.3 * 0.25 * 0.5 + + } + + } +}