diff --git a/CPR2U-iOS/.github/workflows/Build.yml b/CPR2U-iOS/.github/workflows/Build.yml index 2a52114..62dc7bf 100644 --- a/CPR2U-iOS/.github/workflows/Build.yml +++ b/CPR2U-iOS/.github/workflows/Build.yml @@ -14,6 +14,23 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Make Plist Files + run: | + echo ' + + + + + baseURL + $secrets.BASE_URL + googleMapsAPIKey + $secrets.MAPS_API_KEY + + + ' >> ./CPR2U/CPR2U/Private.plist + echo ' + ' >> ./CPR2U/CPR2U/GoogleService-Info.plist + - name: Build Xcode run: | pod install --repo-update --clean-install --project-directory=CPR2U/ diff --git a/CPR2U-iOS/CPR2U/CPR2U.xcodeproj/project.pbxproj b/CPR2U-iOS/CPR2U/CPR2U.xcodeproj/project.pbxproj index 0d62574..d8d1a60 100644 --- a/CPR2U-iOS/CPR2U/CPR2U.xcodeproj/project.pbxproj +++ b/CPR2U-iOS/CPR2U/CPR2U.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 24CEFDC9C69B23440E07EF1C /* Pods_CPR2U.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDF82C0EF3B44D7FED6D1535 /* Pods_CPR2U.framework */; }; + 02108CE8345894DF17CAB194 /* Pods_CPR2U.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3C053A4CB61D3AF4E05B5A2 /* Pods_CPR2U.framework */; }; 3A22BF4329BC9122004411BE /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A22BF4229BC9122004411BE /* URLs.swift */; }; 3A22BF5029BCCE07004411BE /* AuthEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A22BF4F29BCCE07004411BE /* AuthEndPoint.swift */; }; 3A22BF5229BCD6B8004411BE /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A22BF5129BCD6B8004411BE /* AuthManager.swift */; }; @@ -19,6 +19,10 @@ 3A22BF6529BE01AB004411BE /* EndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A22BF6429BE01AB004411BE /* EndPoint.swift */; }; 3A22BF6729BE05D9004411BE /* Encodable+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A22BF6629BE05D9004411BE /* Encodable+.swift */; }; 3A22BF6929BE0A87004411BE /* Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A22BF6829BE0A87004411BE /* Requestable.swift */; }; + 3A2FF4C02A0CC9D2001F6132 /* Private.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3A396BAC29CA138A009B0545 /* Private.plist */; }; + 3A2FF4C12A0CC9D8001F6132 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3A70257929D4731B00225F56 /* GoogleService-Info.plist */; }; + 3A2FF4C62A0D0EDF001F6132 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3A2FF4C82A0D0EDF001F6132 /* Localizable.strings */; }; + 3A2FF4CA2A0D1890001F6132 /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2FF4C92A0D1890001F6132 /* String+.swift */; }; 3A324CD029C4BCCF00165E2E /* OXQuizChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A324CCF29C4BCCF00165E2E /* OXQuizChoiceView.swift */; }; 3A324CD229C4BCE000165E2E /* MultiQuizChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A324CD129C4BCE000165E2E /* MultiQuizChoiceView.swift */; }; 3A324CD829C60E6400165E2E /* movenet_thunder.tflite in Resources */ = {isa = PBXBuildFile; fileRef = 3A324CD629C60E6400165E2E /* movenet_thunder.tflite */; }; @@ -31,6 +35,7 @@ 3A324CE729C6104D00165E2E /* Data+TFLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A324CE629C6104D00165E2E /* Data+TFLite.swift */; }; 3A324CEA29C6111200165E2E /* CameraFeedManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A324CE929C6111200165E2E /* CameraFeedManager.swift */; }; 3A324CEC29C611AB00165E2E /* CameraOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A324CEB29C611AB00165E2E /* CameraOverlayView.swift */; }; + 3A36AB512A263A8F001BE444 /* StatusCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A36AB502A263A8F001BE444 /* StatusCircleView.swift */; }; 3A396B8F29C89DA3009B0545 /* EducationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A396B8E29C89DA3009B0545 /* EducationViewModel.swift */; }; 3A396B9129C8A87D009B0545 /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A396B9029C8A87D009B0545 /* ViewModelProtocol.swift */; }; 3A396B9329C97E31009B0545 /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A396B9229C97E31009B0545 /* UIView+.swift */; }; @@ -62,14 +67,11 @@ 3A70256B29D4292E00225F56 /* AddressEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70256A29D4292E00225F56 /* AddressEndPoint.swift */; }; 3A70256D29D4293800225F56 /* AddressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70256C29D4293800225F56 /* AddressManager.swift */; }; 3A70256F29D4293E00225F56 /* Address.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70256E29D4293E00225F56 /* Address.swift */; }; - 3A70257329D42DB300225F56 /* AddressSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70257229D42DB300225F56 /* AddressSettingView.swift */; }; - 3A70258529D4C12100225F56 /* CPR_Posture_Sound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 3A70258429D4C12100225F56 /* CPR_Posture_Sound.mp3 */; }; 3A70258929D5BD9A00225F56 /* DispatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70258829D5BD9A00225F56 /* DispatchViewController.swift */; }; 3A70258B29D5BFA300225F56 /* DispatchDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70258A29D5BFA300225F56 /* DispatchDescriptionView.swift */; }; 3A70259029D5CAF300225F56 /* DispatchEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70258F29D5CAF300225F56 /* DispatchEndPoint.swift */; }; 3A70259229D5CAFE00225F56 /* DispatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70259129D5CAFE00225F56 /* DispatchManager.swift */; }; 3A70259429D5CC6100225F56 /* Dispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70259329D5CC6100225F56 /* Dispatch.swift */; }; - 3A70259629D5E07700225F56 /* DispatchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A70259529D5E07700225F56 /* DispatchViewModel.swift */; }; 3A8C31A129D6042B004A4B84 /* MypageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8C31A029D6042B004A4B84 /* MypageViewController.swift */; }; 3A8C31A429D6062E004A4B84 /* MypageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8C31A329D6062E004A4B84 /* MypageStatusView.swift */; }; 3A8C31AB29D62FF1004A4B84 /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8C31AA29D62FF1004A4B84 /* ReportViewController.swift */; }; @@ -79,6 +81,7 @@ 3A91C2CF29D0396500028003 /* CallEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A91C2CE29D0396500028003 /* CallEndPoint.swift */; }; 3A91C2D129D0396E00028003 /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A91C2D029D0396E00028003 /* CallManager.swift */; }; 3A91C2D329D03FCC00028003 /* CallerLocationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A91C2D229D03FCC00028003 /* CallerLocationInfo.swift */; }; + 3AA3CD832A29B96B006AC4D1 /* CPR_Posture_Sound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 3AA3CD822A29B96B006AC4D1 /* CPR_Posture_Sound.mp3 */; }; 3AA636AE29AE5055006BB5EF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636AD29AE5055006BB5EF /* AppDelegate.swift */; }; 3AA636B029AE5055006BB5EF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636AF29AE5055006BB5EF /* SceneDelegate.swift */; }; 3AA636B729AE5057006BB5EF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AA636B629AE5057006BB5EF /* Assets.xcassets */; }; @@ -93,7 +96,6 @@ 3AA636D929B36C76006BB5EF /* NicknameVertificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636D829B36C76006BB5EF /* NicknameVertificationViewController.swift */; }; 3AA636ED29B5E816006BB5EF /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636EC29B5E816006BB5EF /* AuthViewModel.swift */; }; 3AA636F129B5F1C8006BB5EF /* UIControl+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636F029B5F1C8006BB5EF /* UIControl+.swift */; }; - 3AA636F329B5F20D006BB5EF /* UITextField+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636F229B5F20D006BB5EF /* UITextField+.swift */; }; 3AA636F529B61D67006BB5EF /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA636F429B61D67006BB5EF /* UIViewController+.swift */; }; 3ADE724F29B9D4AB00EEE19C /* Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE724E29B9D4AB00EEE19C /* Constraints.swift */; }; 3ADE725229B9D55100EEE19C /* EducationMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE725129B9D55100EEE19C /* EducationMainViewController.swift */; }; @@ -109,10 +111,15 @@ 3ADE727029BB630F00EEE19C /* PosePracticeResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE726F29BB630F00EEE19C /* PosePracticeResultViewController.swift */; }; 3ADE727229BB64D400EEE19C /* EvaluationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE727129BB64D400EEE19C /* EvaluationResultView.swift */; }; 3ADE727629BB78BF00EEE19C /* ScoreResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE727529BB78BF00EEE19C /* ScoreResultView.swift */; }; + 3AF695252A1DD54E001CA915 /* PoseAvailabilityCheckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF695242A1DD54E001CA915 /* PoseAvailabilityCheckView.swift */; }; + 3AF695272A1DD567001CA915 /* PosePracticeCountDownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF695262A1DD567001CA915 /* PosePracticeCountDownView.swift */; }; + 3AF695292A1DFE03001CA915 /* AddressVerificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF695282A1DFE03001CA915 /* AddressVerificationViewController.swift */; }; + 3AF6952B2A1E3C44001CA915 /* UITextField+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF6952A2A1E3C44001CA915 /* UITextField+.swift */; }; + 3AF6DEB42A293D7600FE5104 /* (null) in Resources */ = {isa = PBXBuildFile; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 2B9F1310FB54413A2CCE87FD /* Pods-CPR2U.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CPR2U.debug.xcconfig"; path = "Target Support Files/Pods-CPR2U/Pods-CPR2U.debug.xcconfig"; sourceTree = ""; }; + 322A1B8B76D737CA33C5BB8F /* Pods-CPR2U.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CPR2U.release.xcconfig"; path = "Target Support Files/Pods-CPR2U/Pods-CPR2U.release.xcconfig"; sourceTree = ""; }; 3A22BF4229BC9122004411BE /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; 3A22BF4F29BCCE07004411BE /* AuthEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthEndPoint.swift; sourceTree = ""; }; 3A22BF5129BCD6B8004411BE /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = ""; }; @@ -124,6 +131,8 @@ 3A22BF6429BE01AB004411BE /* EndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPoint.swift; sourceTree = ""; }; 3A22BF6629BE05D9004411BE /* Encodable+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+.swift"; sourceTree = ""; }; 3A22BF6829BE0A87004411BE /* Requestable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Requestable.swift; sourceTree = ""; }; + 3A2FF4C72A0D0EDF001F6132 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 3A2FF4C92A0D1890001F6132 /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; 3A324CCF29C4BCCF00165E2E /* OXQuizChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OXQuizChoiceView.swift; sourceTree = ""; }; 3A324CD129C4BCE000165E2E /* MultiQuizChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiQuizChoiceView.swift; sourceTree = ""; }; 3A324CD629C60E6400165E2E /* movenet_thunder.tflite */ = {isa = PBXFileReference; lastKnownFileType = file; path = movenet_thunder.tflite; sourceTree = ""; }; @@ -136,6 +145,7 @@ 3A324CE629C6104D00165E2E /* Data+TFLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+TFLite.swift"; sourceTree = ""; }; 3A324CE929C6111200165E2E /* CameraFeedManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraFeedManager.swift; sourceTree = ""; }; 3A324CEB29C611AB00165E2E /* CameraOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraOverlayView.swift; sourceTree = ""; }; + 3A36AB502A263A8F001BE444 /* StatusCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCircleView.swift; sourceTree = ""; }; 3A396B8E29C89DA3009B0545 /* EducationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EducationViewModel.swift; sourceTree = ""; }; 3A396B9029C8A87D009B0545 /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = ""; }; 3A396B9229C97E31009B0545 /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; }; @@ -167,15 +177,12 @@ 3A70256A29D4292E00225F56 /* AddressEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressEndPoint.swift; sourceTree = ""; }; 3A70256C29D4293800225F56 /* AddressManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressManager.swift; sourceTree = ""; }; 3A70256E29D4293E00225F56 /* Address.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Address.swift; sourceTree = ""; }; - 3A70257229D42DB300225F56 /* AddressSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSettingView.swift; sourceTree = ""; }; 3A70257929D4731B00225F56 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 3A70258429D4C12100225F56 /* CPR_Posture_Sound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = CPR_Posture_Sound.mp3; sourceTree = ""; }; 3A70258829D5BD9A00225F56 /* DispatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchViewController.swift; sourceTree = ""; }; 3A70258A29D5BFA300225F56 /* DispatchDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchDescriptionView.swift; sourceTree = ""; }; 3A70258F29D5CAF300225F56 /* DispatchEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchEndPoint.swift; sourceTree = ""; }; 3A70259129D5CAFE00225F56 /* DispatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchManager.swift; sourceTree = ""; }; 3A70259329D5CC6100225F56 /* Dispatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dispatch.swift; sourceTree = ""; }; - 3A70259529D5E07700225F56 /* DispatchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchViewModel.swift; sourceTree = ""; }; 3A8C31A029D6042B004A4B84 /* MypageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MypageViewController.swift; sourceTree = ""; }; 3A8C31A329D6062E004A4B84 /* MypageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MypageStatusView.swift; sourceTree = ""; }; 3A8C31AA29D62FF1004A4B84 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = ""; }; @@ -185,6 +192,7 @@ 3A91C2CE29D0396500028003 /* CallEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallEndPoint.swift; sourceTree = ""; }; 3A91C2D029D0396E00028003 /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; 3A91C2D229D03FCC00028003 /* CallerLocationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallerLocationInfo.swift; sourceTree = ""; }; + 3AA3CD822A29B96B006AC4D1 /* CPR_Posture_Sound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = CPR_Posture_Sound.mp3; path = ../../../../../../2023_GDSC_Solution_Challenge/Midi/CPR_Posture_Sound.mp3; sourceTree = ""; }; 3AA636AA29AE5055006BB5EF /* CPR2U.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CPR2U.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3AA636AD29AE5055006BB5EF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3AA636AF29AE5055006BB5EF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -201,7 +209,6 @@ 3AA636D829B36C76006BB5EF /* NicknameVertificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameVertificationViewController.swift; sourceTree = ""; }; 3AA636EC29B5E816006BB5EF /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; 3AA636F029B5F1C8006BB5EF /* UIControl+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+.swift"; sourceTree = ""; }; - 3AA636F229B5F20D006BB5EF /* UITextField+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+.swift"; sourceTree = ""; }; 3AA636F429B61D67006BB5EF /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; 3ADE724E29B9D4AB00EEE19C /* Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constraints.swift; sourceTree = ""; }; 3ADE725129B9D55100EEE19C /* EducationMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EducationMainViewController.swift; sourceTree = ""; }; @@ -217,8 +224,12 @@ 3ADE726F29BB630F00EEE19C /* PosePracticeResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosePracticeResultViewController.swift; sourceTree = ""; }; 3ADE727129BB64D400EEE19C /* EvaluationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvaluationResultView.swift; sourceTree = ""; }; 3ADE727529BB78BF00EEE19C /* ScoreResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreResultView.swift; sourceTree = ""; }; - 8603206BC53B2CC2028A3EFB /* Pods-CPR2U.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CPR2U.release.xcconfig"; path = "Target Support Files/Pods-CPR2U/Pods-CPR2U.release.xcconfig"; sourceTree = ""; }; - EDF82C0EF3B44D7FED6D1535 /* Pods_CPR2U.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CPR2U.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AF695242A1DD54E001CA915 /* PoseAvailabilityCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoseAvailabilityCheckView.swift; sourceTree = ""; }; + 3AF695262A1DD567001CA915 /* PosePracticeCountDownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosePracticeCountDownView.swift; sourceTree = ""; }; + 3AF695282A1DFE03001CA915 /* AddressVerificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressVerificationViewController.swift; sourceTree = ""; }; + 3AF6952A2A1E3C44001CA915 /* UITextField+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+.swift"; sourceTree = ""; }; + 9E55296996EBDD8450721680 /* Pods-CPR2U.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CPR2U.debug.xcconfig"; path = "Target Support Files/Pods-CPR2U/Pods-CPR2U.debug.xcconfig"; sourceTree = ""; }; + A3C053A4CB61D3AF4E05B5A2 /* Pods_CPR2U.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CPR2U.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -228,7 +239,7 @@ files = ( 3A396B9B29C9D395009B0545 /* FirebaseMessaging in Frameworks */, 3A5C390E29C0844700378015 /* CombineCocoa in Frameworks */, - 24CEFDC9C69B23440E07EF1C /* Pods_CPR2U.framework in Frameworks */, + 02108CE8345894DF17CAB194 /* Pods_CPR2U.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -238,8 +249,8 @@ 16C861399981B0664D00A6AF /* Pods */ = { isa = PBXGroup; children = ( - 2B9F1310FB54413A2CCE87FD /* Pods-CPR2U.debug.xcconfig */, - 8603206BC53B2CC2028A3EFB /* Pods-CPR2U.release.xcconfig */, + 9E55296996EBDD8450721680 /* Pods-CPR2U.debug.xcconfig */, + 322A1B8B76D737CA33C5BB8F /* Pods-CPR2U.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -282,7 +293,6 @@ 3A22BF3E29BC8C62004411BE /* Extension */ = { isa = PBXGroup; children = ( - 3AA636F229B5F20D006BB5EF /* UITextField+.swift */, 3AA636C129B07C5F006BB5EF /* UIColor+.swift */, 3AA636D229B24D72006BB5EF /* UIFont+.swift */, 3AA636F029B5F1C8006BB5EF /* UIControl+.swift */, @@ -292,6 +302,8 @@ 3A396B9229C97E31009B0545 /* UIView+.swift */, 3A396BA429CA0646009B0545 /* UIWindow+.swift */, 3A396BD829CF644A009B0545 /* Int+.swift */, + 3A2FF4C92A0D1890001F6132 /* String+.swift */, + 3AF6952A2A1E3C44001CA915 /* UITextField+.swift */, ); path = Extension; sourceTree = ""; @@ -539,7 +551,7 @@ isa = PBXGroup; children = ( 3A8C31B729D6FAAB004A4B84 /* CPR_Sound.mp3 */, - 3A70258429D4C12100225F56 /* CPR_Posture_Sound.mp3 */, + 3AA3CD822A29B96B006AC4D1 /* CPR_Posture_Sound.mp3 */, ); path = Sound; sourceTree = ""; @@ -549,7 +561,6 @@ children = ( 3A70258F29D5CAF300225F56 /* DispatchEndPoint.swift */, 3A70259129D5CAFE00225F56 /* DispatchManager.swift */, - 3A70259529D5E07700225F56 /* DispatchViewModel.swift */, ); path = Dispatch; sourceTree = ""; @@ -579,7 +590,7 @@ 3AA636AC29AE5055006BB5EF /* CPR2U */, 3AA636AB29AE5055006BB5EF /* Products */, 16C861399981B0664D00A6AF /* Pods */, - A7D4AB3E214BA6052EC3FA37 /* Frameworks */, + FCD2D0EE2775B9F31DE32B99 /* Frameworks */, ); sourceTree = ""; }; @@ -608,6 +619,7 @@ 3AA636BB29AE5057006BB5EF /* Info.plist */, 3A396BAC29CA138A009B0545 /* Private.plist */, 3A70257929D4731B00225F56 /* GoogleService-Info.plist */, + 3A2FF4C82A0D0EDF001F6132 /* Localizable.strings */, ); path = CPR2U; sourceTree = ""; @@ -624,6 +636,7 @@ 3AA636DE29B37E04006BB5EF /* Login */ = { isa = PBXGroup; children = ( + 3AA636EC29B5E816006BB5EF /* AuthViewModel.swift */, 3AA636DF29B37E21006BB5EF /* View */, ); path = Login; @@ -636,7 +649,7 @@ 3AA636D429B358D0006BB5EF /* SMSCodeVertificationViewController.swift */, 3AA636D829B36C76006BB5EF /* NicknameVertificationViewController.swift */, 3AA636D629B35CA3006BB5EF /* SMSCodeInputView.swift */, - 3AA636EC29B5E816006BB5EF /* AuthViewModel.swift */, + 3AF695282A1DFE03001CA915 /* AddressVerificationViewController.swift */, ); path = View; sourceTree = ""; @@ -660,7 +673,7 @@ 3ADE725329B9D69D00EEE19C /* CertificateStatusView.swift */, 3ADE725529B9EA3200EEE19C /* EducationProgressView.swift */, 3ADE725929BA333D00EEE19C /* EducationCollectionViewCell.swift */, - 3A70257229D42DB300225F56 /* AddressSettingView.swift */, + 3A36AB502A263A8F001BE444 /* StatusCircleView.swift */, ); path = Main; sourceTree = ""; @@ -688,14 +701,16 @@ 3ADE727129BB64D400EEE19C /* EvaluationResultView.swift */, 3ADE727529BB78BF00EEE19C /* ScoreResultView.swift */, 3A324CEB29C611AB00165E2E /* CameraOverlayView.swift */, + 3AF695242A1DD54E001CA915 /* PoseAvailabilityCheckView.swift */, + 3AF695262A1DD567001CA915 /* PosePracticeCountDownView.swift */, ); path = Pose; sourceTree = ""; }; - A7D4AB3E214BA6052EC3FA37 /* Frameworks */ = { + FCD2D0EE2775B9F31DE32B99 /* Frameworks */ = { isa = PBXGroup; children = ( - EDF82C0EF3B44D7FED6D1535 /* Pods_CPR2U.framework */, + A3C053A4CB61D3AF4E05B5A2 /* Pods_CPR2U.framework */, ); name = Frameworks; sourceTree = ""; @@ -707,11 +722,11 @@ isa = PBXNativeTarget; buildConfigurationList = 3AA636BE29AE5057006BB5EF /* Build configuration list for PBXNativeTarget "CPR2U" */; buildPhases = ( - 379DD7D0D7CA53D856B6005F /* [CP] Check Pods Manifest.lock */, + 7B0D1DA42150E3288153902D /* [CP] Check Pods Manifest.lock */, 3AA636A629AE5055006BB5EF /* Sources */, 3AA636A729AE5055006BB5EF /* Frameworks */, 3AA636A829AE5055006BB5EF /* Resources */, - 59C2C471D36B615BE19C5723 /* [CP] Copy Pods Resources */, + 84A63088F34A862DBA6F668B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -733,7 +748,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1400; + LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1400; TargetAttributes = { 3AA636A929AE5055006BB5EF = { @@ -768,12 +783,16 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3A2FF4C12A0CC9D8001F6132 /* GoogleService-Info.plist in Resources */, + 3A2FF4C02A0CC9D2001F6132 /* Private.plist in Resources */, 3A8C31B829D6FAAB004A4B84 /* CPR_Sound.mp3 in Resources */, - 3A70258529D4C12100225F56 /* CPR_Posture_Sound.mp3 in Resources */, + 3AF6DEB42A293D7600FE5104 /* (null) in Resources */, 3AA636C929B08402006BB5EF /* NotoSans-Regular.ttf in Resources */, + 3A2FF4C62A0D0EDF001F6132 /* Localizable.strings in Resources */, 3AA636C829B08402006BB5EF /* NotoSans-Bold.ttf in Resources */, 3A324CD829C60E6400165E2E /* movenet_thunder.tflite in Resources */, 3AA636BA29AE5057006BB5EF /* LaunchScreen.storyboard in Resources */, + 3AA3CD832A29B96B006AC4D1 /* CPR_Posture_Sound.mp3 in Resources */, 3AA636B729AE5057006BB5EF /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -781,7 +800,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 379DD7D0D7CA53D856B6005F /* [CP] Check Pods Manifest.lock */ = { + 7B0D1DA42150E3288153902D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -803,7 +822,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 59C2C471D36B615BE19C5723 /* [CP] Copy Pods Resources */ = { + 84A63088F34A862DBA6F668B /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -832,6 +851,7 @@ 3A8C31A429D6062E004A4B84 /* MypageStatusView.swift in Sources */, 3ADE725629B9EA3200EEE19C /* EducationProgressView.swift in Sources */, 3A396BD329CF3301009B0545 /* TimeCounterView.swift in Sources */, + 3AF695252A1DD54E001CA915 /* PoseAvailabilityCheckView.swift in Sources */, 3ADE726029BB03A100EEE19C /* EducationQuizViewController.swift in Sources */, 3A22BF5D29BCF159004411BE /* Auth.swift in Sources */, 3A91C2D329D03FCC00028003 /* CallerLocationInfo.swift in Sources */, @@ -842,6 +862,7 @@ 3A324CDD29C60E9800165E2E /* MoveNet.swift in Sources */, 3A70255E29D0869200225F56 /* MapManager.swift in Sources */, 3ADE725229B9D55100EEE19C /* EducationMainViewController.swift in Sources */, + 3AF6952B2A1E3C44001CA915 /* UITextField+.swift in Sources */, 3A396BC329CCA563009B0545 /* LectureViewController.swift in Sources */, 3A396BB729CB1F38009B0545 /* EducationManager.swift in Sources */, 3A70256F29D4293E00225F56 /* Address.swift in Sources */, @@ -851,7 +872,6 @@ 3ADE726229BB047300EEE19C /* QuizChoiceView.swift in Sources */, 3AA636F529B61D67006BB5EF /* UIViewController+.swift in Sources */, 3A396B8F29C89DA3009B0545 /* EducationViewModel.swift in Sources */, - 3AA636F329B5F20D006BB5EF /* UITextField+.swift in Sources */, 3A5C391029C08C8900378015 /* QuizViewModel.swift in Sources */, 3ADE726629BB04C900EEE19C /* QuizQuestionView.swift in Sources */, 3A22BF5529BCEAC8004411BE /* NetworkResponse.swift in Sources */, @@ -874,7 +894,6 @@ 3A396BAA29CA0A07009B0545 /* TabBarViewController.swift in Sources */, 3AA636D729B35CA3006BB5EF /* SMSCodeInputView.swift in Sources */, 3A22BF6929BE0A87004411BE /* Requestable.swift in Sources */, - 3A70259629D5E07700225F56 /* DispatchViewModel.swift in Sources */, 3A22BF5729BCEBA2004411BE /* APIError.swift in Sources */, 3ADE725429B9D69D00EEE19C /* CertificateStatusView.swift in Sources */, 3ADE726E29BB59C000EEE19C /* PosePracticeViewController.swift in Sources */, @@ -894,6 +913,7 @@ 3ADE724F29B9D4AB00EEE19C /* Constraints.swift in Sources */, 3A324CE529C6104400165E2E /* CVPixelBuffer+TFLite.swift in Sources */, 3AA636C229B07C5F006BB5EF /* UIColor+.swift in Sources */, + 3AF695272A1DD567001CA915 /* PosePracticeCountDownView.swift in Sources */, 3A22BF5029BCCE07004411BE /* AuthEndPoint.swift in Sources */, 3A396BB529CB1F31009B0545 /* EducationEndPoint.swift in Sources */, 3A8C31A129D6042B004A4B84 /* MypageViewController.swift in Sources */, @@ -904,12 +924,15 @@ 3A324CE329C6103800165E2E /* CGSize+TFLite.swift in Sources */, 3A91C2D129D0396E00028003 /* CallManager.swift in Sources */, 3AA636D329B24D72006BB5EF /* UIFont+.swift in Sources */, + 3A36AB512A263A8F001BE444 /* StatusCircleView.swift in Sources */, + 3A2FF4CA2A0D1890001F6132 /* String+.swift in Sources */, 3A70259429D5CC6100225F56 /* Dispatch.swift in Sources */, 3A22BF6129BDFF3B004411BE /* HttpMethod.swift in Sources */, 3A70258B29D5BFA300225F56 /* DispatchDescriptionView.swift in Sources */, 3A70256B29D4292E00225F56 /* AddressEndPoint.swift in Sources */, 3A324CE129C60EAC00165E2E /* PoseEstimator.swift in Sources */, 3ADE727629BB78BF00EEE19C /* ScoreResultView.swift in Sources */, + 3AF695292A1DFE03001CA915 /* AddressVerificationViewController.swift in Sources */, 3AA636B029AE5055006BB5EF /* SceneDelegate.swift in Sources */, 3A396B9129C8A87D009B0545 /* ViewModelProtocol.swift in Sources */, 3A22BF5F29BDFF1A004411BE /* NetworkRequest.swift in Sources */, @@ -917,7 +940,6 @@ 3A324CD229C4BCE000165E2E /* MultiQuizChoiceView.swift in Sources */, 3A324CEA29C6111200165E2E /* CameraFeedManager.swift in Sources */, 3A396BD129CF2BA2009B0545 /* CallCircleView.swift in Sources */, - 3A70257329D42DB300225F56 /* AddressSettingView.swift in Sources */, 3A22BF6729BE05D9004411BE /* Encodable+.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -925,6 +947,14 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ + 3A2FF4C82A0D0EDF001F6132 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 3A2FF4C72A0D0EDF001F6132 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; 3AA636B829AE5057006BB5EF /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -1052,8 +1082,9 @@ }; 3AA636BF29AE5057006BB5EF /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2B9F1310FB54413A2CCE87FD /* Pods-CPR2U.debug.xcconfig */; + baseConfigurationReference = 9E55296996EBDD8450721680 /* Pods-CPR2U.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = CPR2U/CPR2U.entitlements; @@ -1083,8 +1114,9 @@ }; 3AA636C029AE5057006BB5EF /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8603206BC53B2CC2028A3EFB /* Pods-CPR2U.release.xcconfig */; + baseConfigurationReference = 322A1B8B76D737CA33C5BB8F /* Pods-CPR2U.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = CPR2U/CPR2U.entitlements; diff --git a/CPR2U-iOS/CPR2U/CPR2U/Application/AppDelegate.swift b/CPR2U-iOS/CPR2U/CPR2U/Application/AppDelegate.swift index 96cfd77..daae284 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Application/AppDelegate.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Application/AppDelegate.swift @@ -16,6 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // https://stackoverflow.com/questions/28938660/how-to-lock-orientation-of-one-view-controller-to-portrait-mode-only-in-swift var orientationLock = UIInterfaceOrientationMask.portrait + var isNotificationHandled = false func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return self.orientationLock @@ -24,13 +25,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - registerForRemoteNotifications() - // MARK: FCM Setting FirebaseApp.configure() - Messaging.messaging().delegate = self UNUserNotificationCenter.current().delegate = self + Messaging.messaging().delegate = self let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization( @@ -57,32 +56,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - let deviceTokenString = deviceToken.map { String(format: "%02x", $0) }.joined() - Messaging.messaging().apnsToken = deviceToken - } - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { } - - private func registerForRemoteNotifications() { - - // 1. 푸시 center (유저에게 권한 요청 용도) - let center = UNUserNotificationCenter.current() - center.delegate = self // push처리에 대한 delegate - UNUserNotificationCenterDelegate - let options: UNAuthorizationOptions = [.alert, .sound, .badge] - center.requestAuthorization(options: options) { (granted, error) in - - guard granted else { - return - } - - DispatchQueue.main.async { - // 2. APNs에 디바이스 토큰 등록 - UIApplication.shared.registerForRemoteNotifications() - } - } - } } extension AppDelegate: MessagingDelegate { @@ -91,6 +66,7 @@ extension AppDelegate: MessagingDelegate { let dataDict:[String: String] = ["token": fcmToken] NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict) DeviceTokenManager.deviceToken = fcmToken + } } @@ -100,12 +76,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.alert, .badge, .sound]) + completionHandler([.banner, .sound]) } - + // push를 탭한 경우 처리 func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - let url = response.notification.request.content.userInfo + let userInfo = response.notification.request.content.userInfo + print(response.notification) + NotificationCenter.default.post(name: Notification.Name("ShowCallerPage"), object: nil, userInfo: userInfo) } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Application/SceneDelegate.swift b/CPR2U-iOS/CPR2U/CPR2U/Application/SceneDelegate.swift index 92a1497..bd97329 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Application/SceneDelegate.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Application/SceneDelegate.swift @@ -17,7 +17,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let window = UIWindow(windowScene: windowScene) self.window = window -// let navVC = UINavigationController(rootViewController: EducationMainViewController()) let vc = AutoLoginViewController() window.rootViewController = vc window.backgroundColor = .white diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check.imageset/Contents.json similarity index 89% rename from CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map.imageset/Contents.json rename to CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check.imageset/Contents.json index c78b381..bd23e48 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map.imageset/Contents.json +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "map.png", + "filename" : "check.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check.imageset/check.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check.imageset/check.png new file mode 100644 index 0000000..ba096d6 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check.imageset/check.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/fail_heart.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check_badge.imageset/Contents.json similarity index 87% rename from CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/fail_heart.imageset/Contents.json rename to CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check_badge.imageset/Contents.json index 83386f1..e61f67d 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/fail_heart.imageset/Contents.json +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check_badge.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_fail_heart.png", + "filename" : "check_badge.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check_badge.imageset/check_badge.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check_badge.imageset/check_badge.png new file mode 100644 index 0000000..912feb7 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/check_badge.imageset/check_badge.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/exclamation_mark.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/exclamation_mark.imageset/Contents.json new file mode 100644 index 0000000..5a52b3c --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/exclamation_mark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "exclamation_mark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/exclamation_mark.imageset/exclamation_mark.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/exclamation_mark.imageset/exclamation_mark.png new file mode 100644 index 0000000..6cd89b1 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/exclamation_mark.imageset/exclamation_mark.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/fail_heart.imageset/ic_fail_heart.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/fail_heart.imageset/ic_fail_heart.png deleted file mode 100644 index 69cc0c2..0000000 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/fail_heart.imageset/ic_fail_heart.png and /dev/null differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_check.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_fail.imageset/Contents.json similarity index 87% rename from CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_check.imageset/Contents.json rename to CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_fail.imageset/Contents.json index 9a20698..5f65819 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_check.imageset/Contents.json +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_fail.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "time_check.png", + "filename" : "heart_fail.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_fail.imageset/heart_fail.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_fail.imageset/heart_fail.png new file mode 100644 index 0000000..5c7415e Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_fail.imageset/heart_fail.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person.imageset/heart_person.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person.imageset/heart_person.png index 47d2b2b..dc25d15 100644 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person.imageset/heart_person.png and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person.imageset/heart_person.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person_big.imageset/heart_person_big.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person_big.imageset/heart_person_big.png index ccfbdc2..30857aa 100644 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person_big.imageset/heart_person_big.png and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_person_big.imageset/heart_person_big.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/success_heart.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_success.imageset/Contents.json similarity index 87% rename from CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/success_heart.imageset/Contents.json rename to CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_success.imageset/Contents.json index b14ca0d..80bb5d8 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/success_heart.imageset/Contents.json +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_success.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "success_heart.png", + "filename" : "heart_success.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_success.imageset/heart_success.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_success.imageset/heart_success.png new file mode 100644 index 0000000..7c57259 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/heart_success.imageset/heart_success.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map.imageset/map.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map.imageset/map.png deleted file mode 100644 index 943ca72..0000000 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map.imageset/map.png and /dev/null differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map_black.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map_black.imageset/Contents.json new file mode 100644 index 0000000..ffc7f8b --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map_black.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "map_black.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map_black.imageset/map_black.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map_black.imageset/map_black.png new file mode 100644 index 0000000..eebd044 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/map_black.imageset/map_black.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person.imageset/person.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person.imageset/person.png index 8e1c48a..05194ae 100644 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person.imageset/person.png and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person.imageset/person.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person_big.imageset/person_big.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person_big.imageset/person_big.png index c699857..0885f0d 100644 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person_big.imageset/person_big.png and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/person_big.imageset/person_big.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/pose_guideline.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/pose_guideline.imageset/Contents.json new file mode 100644 index 0000000..de85ad9 --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/pose_guideline.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pose_guideline.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/pose_guideline.imageset/pose_guideline.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/pose_guideline.imageset/pose_guideline.png new file mode 100644 index 0000000..9fb1066 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/pose_guideline.imageset/pose_guideline.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/success_heart.imageset/success_heart.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/success_heart.imageset/success_heart.png deleted file mode 100644 index 5963aee..0000000 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/success_heart.imageset/success_heart.png and /dev/null differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time.imageset/time.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time.imageset/time.png index db9e2c1..af0f6c6 100644 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time.imageset/time.png and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time.imageset/time.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_black.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_black.imageset/Contents.json new file mode 100644 index 0000000..81dc2fc --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_black.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "time_black.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_black.imageset/time_black.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_black.imageset/time_black.png new file mode 100644 index 0000000..e68db52 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_black.imageset/time_black.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_check.imageset/time_check.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_check.imageset/time_check.png deleted file mode 100644 index 8491cb2..0000000 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/time_check.imageset/time_check.png and /dev/null differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/x_mark.imageset/Contents.json b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/x_mark.imageset/Contents.json new file mode 100644 index 0000000..d58ea8a --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/x_mark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "x_mark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/x_mark.imageset/x_mark.png b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/x_mark.imageset/x_mark.png new file mode 100644 index 0000000..569bab2 Binary files /dev/null and b/CPR2U-iOS/CPR2U/CPR2U/Assets.xcassets/x_mark.imageset/x_mark.png differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Component/Sound/CPR_Posture_Sound.mp3 b/CPR2U-iOS/CPR2U/CPR2U/Component/Sound/CPR_Posture_Sound.mp3 index 5eca1cb..4624a26 100644 Binary files a/CPR2U-iOS/CPR2U/CPR2U/Component/Sound/CPR_Posture_Sound.mp3 and b/CPR2U-iOS/CPR2U/CPR2U/Component/Sound/CPR_Posture_Sound.mp3 differ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Auth.swift b/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Auth.swift index 8ad7ec1..f02ebd9 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Auth.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Auth.swift @@ -27,3 +27,5 @@ struct SMSCodeResult: Codable { } struct NicknameVerifyResult: Codable {} + +struct LogOutResult: Codable {} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Education.swift b/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Education.swift index bec599c..a300449 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Education.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Data/DTO/Education.swift @@ -18,7 +18,6 @@ struct UserInfo: Codable { let angel_status: Int let progress_percent: Double let is_lecture_completed: Int - let last_lecture_title: String let is_quiz_completed: Int let is_posture_completed: Int let days_left_until_expiration: Int? diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/CallViewModel.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/CallViewModel.swift index 0be158d..9487d70 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/CallViewModel.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/CallViewModel.swift @@ -10,36 +10,38 @@ import Foundation import GoogleMaps final class CallViewModel: OutputOnlyViewModelType { + @Published private(set)var callerListInfo: CallerListInfo? + @Published private(set)var dispatcherCount: Int? + private var callManager: CallManager + private var dispatchManager: DispatchManager private var mapManager: MapManager private var currentLocation: CLLocationCoordinate2D? private var currentLocationAddress = CurrentValueSubject("Unable") - var callerList: CurrentValueSubject? private var callId: Int? private let iscalled = CurrentValueSubject(false) + private let isDispatchEnd = CurrentValueSubject(false) var timer: Timer.TimerPublisher? init() { callManager = CallManager(service: APIManager()) mapManager = MapManager() - Task { - try await receiveCallerList() - } + dispatchManager = DispatchManager(service: APIManager()) + receiveCallerList() setLocation() } struct Output { let isCalled: CurrentValueSubject let currentLocationAddress: CurrentValueSubject? - let callerList: CurrentValueSubject? } func transform() -> Output { - return Output(isCalled: iscalled, currentLocationAddress: currentLocationAddress, callerList: callerList) + return Output(isCalled: iscalled, currentLocationAddress: currentLocationAddress) } func isCallSucceed() { @@ -64,16 +66,13 @@ final class CallViewModel: OutputOnlyViewModelType { currentLocationAddress.send(str) } - func receiveCallerList() async throws -> CallerListInfo? { - let result = Task { () -> CallerListInfo? in + func receiveCallerList() { + Task { let callResult = try await callManager.getCallerList() - guard let list = callResult.data else { return nil } - callerList = CurrentValueSubject(list) - print(callerList) - return list + guard let list = callResult.data else { return } + callerListInfo = list } - return try await result.value } func callDispatcher() async throws { @@ -94,18 +93,47 @@ final class CallViewModel: OutputOnlyViewModelType { } } - func countDispatcher() async throws -> Int? { - guard let callId = callId else { return nil } + func countDispatcher() { + guard let callId = callId else { return } - let result = Task { () -> Int? in + Task { let callResult = try await callManager.countDispatcher(callId: callId) - return callResult.data?.number_of_angels + dispatcherCount = callResult.data?.number_of_angels } - - return try await result.value } private func updateCallId(callId: Int) { self.callId = callId } + + func dispatchEnd(dispatchId: Int) async throws -> Bool { + let taskResult = Task { () -> Bool in + let result = try await dispatchManager.dispatchEnd(dispatchId: dispatchId) + return result.success + } + return try await taskResult.value + } + + func updateDispatchEnd() { + isDispatchEnd.send(true) + + } + + func getDispatchEnd() -> CurrentValueSubject { + return isDispatchEnd + } + func dispatchAccept(cprCallId: Int) async throws -> (Bool, DispatchInfo?) { + let taskResult = Task { () -> (Bool, DispatchInfo?) in + let result = try await dispatchManager.dispatchAccept(cprCallId: cprCallId) + return (result.success, result.data) + } + return try await taskResult.value + } + func userReport(reportInfo: ReportInfo) async throws -> Bool { + let taskResult = Task { () -> Bool in + let result = try await dispatchManager.userReport(reportInfo: reportInfo) + return result.success + } + return try await taskResult.value + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/ApproachNoticeView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/ApproachNoticeView.swift index 36918ff..e602bfe 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/ApproachNoticeView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/ApproachNoticeView.swift @@ -10,27 +10,13 @@ import UIKit final class ApproachNoticeView: UIView { - private let peopleImageView: UIImageView = { - let view = UIImageView() - view.image = UIImage(named: "people.png") - return view - }() - - private lazy var peopleCountLabel: UILabel = { - let label = UILabel() - label.font = UIFont(weight: .bold, size: 48) - label.textColor = .mainRed - label.textAlignment = .center - label.text = "0" - return label - }() - private let approachLabel: UILabel = { let label = UILabel() - label.font = UIFont(weight: .bold, size: 20) + label.font = UIFont(weight: .regular, size: 18) label.textColor = .black - label.textAlignment = .left - label.text = "Approaching" + label.textAlignment = .center + label.numberOfLines = 2 + label.text = "angel_approach_notice_default_txt".localized() return label }() @@ -55,7 +41,7 @@ final class ApproachNoticeView: UIView { button.setTitleColor(.white, for: .normal) button.backgroundColor = .mainRed button.layer.cornerRadius = 27.5 - button.setTitle("SITUATION ENDED", for: .normal) + button.setTitle("siuation_end_des_txt".localized(), for: .normal) return button }() @@ -79,40 +65,6 @@ final class ApproachNoticeView: UIView { private func setUpConstraints() { let make = Constraints.shared - let approachStackView = UIStackView() - approachStackView.axis = NSLayoutConstraint.Axis.horizontal - approachStackView.distribution = UIStackView.Distribution.equalSpacing - approachStackView.alignment = UIStackView.Alignment.center - approachStackView.spacing = 8 - - [ - peopleImageView, - peopleCountLabel, - approachLabel - ].forEach({ - approachStackView.addSubview($0) - $0.translatesAutoresizingMaskIntoConstraints = false - $0.centerYAnchor.constraint(equalTo: approachStackView.centerYAnchor).isActive = true - }) - - NSLayoutConstraint.activate([ - peopleImageView.leadingAnchor.constraint(equalTo: approachStackView.leadingAnchor), - peopleImageView.widthAnchor.constraint(equalToConstant: 50), - peopleImageView.heightAnchor.constraint(equalToConstant: 50) - ]) - - NSLayoutConstraint.activate([ - peopleCountLabel.leadingAnchor.constraint(equalTo: peopleImageView.trailingAnchor), - peopleCountLabel.trailingAnchor.constraint(equalTo: approachLabel.leadingAnchor), - peopleCountLabel.heightAnchor.constraint(equalToConstant: 50) - ]) - - NSLayoutConstraint.activate([ - approachLabel.trailingAnchor.constraint(equalTo: approachStackView.trailingAnchor), - approachLabel.widthAnchor.constraint(equalToConstant: 128), - approachLabel.heightAnchor.constraint(equalToConstant: 50) - ]) - let timeStackView = UIStackView() timeStackView.axis = NSLayoutConstraint.Axis.horizontal timeStackView.distribution = UIStackView.Distribution.equalSpacing @@ -141,7 +93,7 @@ final class ApproachNoticeView: UIView { ]) [ - approachStackView, + approachLabel, timeStackView, situationEndButton ].forEach({ @@ -149,13 +101,6 @@ final class ApproachNoticeView: UIView { $0.translatesAutoresizingMaskIntoConstraints = false }) - NSLayoutConstraint.activate([ - approachStackView.topAnchor.constraint(equalTo: self.topAnchor, constant: 36), - approachStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - approachStackView.widthAnchor.constraint(equalToConstant: 222), - approachStackView.heightAnchor.constraint(equalToConstant: 50) - ]) - NSLayoutConstraint.activate([ timeStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), timeStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor), @@ -163,6 +108,13 @@ final class ApproachNoticeView: UIView { timeStackView.heightAnchor.constraint(equalToConstant: 50) ]) + NSLayoutConstraint.activate([ + approachLabel.bottomAnchor.constraint(equalTo: timeStackView.topAnchor, constant: -make.space12), + approachLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), + approachLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), + approachLabel.heightAnchor.constraint(equalToConstant: 50) + ]) + NSLayoutConstraint.activate([ situationEndButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -make.space8), situationEndButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), @@ -173,7 +125,7 @@ final class ApproachNoticeView: UIView { private func setUpStyle() { backgroundColor = .white - self.layer.cornerRadius = 24 + self.layer.cornerRadius = 32 } @@ -184,6 +136,13 @@ final class ApproachNoticeView: UIView { self.parentViewController().dismiss(animated: true) } }.store(in: &cancellables) + + viewModel.$dispatcherCount + .receive(on: DispatchQueue.main) + .sink { [weak self] count in + guard let count = count else { return } + self?.updateApproachLabelText(count: count) + }.store(in: &cancellables) } private func setTimer() { @@ -192,13 +151,10 @@ final class ApproachNoticeView: UIView { .autoconnect() .scan(0) { counter, _ in counter + 1 } .sink { [self] counter in - if counter % 15 == 0 { Task { - let dispatcherNumber = try await viewModel.countDispatcher() - peopleCountLabel.text = "\(dispatcherNumber ?? 0)" + viewModel.countDispatcher() } - } if counter == 301 { viewModel.timer?.connect().cancel() @@ -207,5 +163,15 @@ final class ApproachNoticeView: UIView { } }.store(in: &cancellables) } + + private func updateApproachLabelText(count: Int) { + if count > 0 { + let localizedStr = String(format: "angel_approach_notice_%dmatched_txt".localized(), count) + approachLabel.text = localizedStr + print(count) + } else { + approachLabel.text = "angel_approach_notice_default_txt".localized() + } + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/DispatchWaitViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/DispatchWaitViewController.swift index a924dc4..2cf2664 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/DispatchWaitViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/DispatchWaitViewController.swift @@ -11,12 +11,20 @@ import UIKit final class DispatchWaitViewController: UIViewController { - private let mainLabel: UILabel = { + private let exclamationImageView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "exclamation_mark.png") + return view + }() + + + private let noticeLabel: UILabel = { let label = UILabel() - label.font = UIFont(weight: .bold, size: 64) + label.font = UIFont(weight: .bold, size: 24) label.textAlignment = .center label.textColor = .white - label.text = "Call" + label.text = "approch_des_txt".localized() + label.numberOfLines = 3 return label }() @@ -55,7 +63,8 @@ final class DispatchWaitViewController: UIViewController { private func setUpConstraints() { [ - mainLabel, + exclamationImageView, + noticeLabel, approachNoticeView, emergencyDescriptionView ].forEach({ @@ -71,9 +80,16 @@ final class DispatchWaitViewController: UIViewController { ]) NSLayoutConstraint.activate([ - mainLabel.bottomAnchor.constraint(equalTo: approachNoticeView.topAnchor, constant: -36), - mainLabel.widthAnchor.constraint(equalToConstant: 200), - mainLabel.heightAnchor.constraint(equalToConstant: 80) + noticeLabel.bottomAnchor.constraint(equalTo: approachNoticeView.topAnchor, constant: -36), + noticeLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), + noticeLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor), + noticeLabel.heightAnchor.constraint(equalToConstant: 108) + ]) + + NSLayoutConstraint.activate([ + exclamationImageView.bottomAnchor.constraint(equalTo: noticeLabel.topAnchor, constant: -Constraints.shared.space24), + exclamationImageView.widthAnchor.constraint(equalToConstant: 36), + exclamationImageView.heightAnchor.constraint(equalToConstant: 36) ]) NSLayoutConstraint.activate([ diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/EmergencyDescriptionView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/EmergencyDescriptionView.swift index eacea91..a9e1a7a 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/EmergencyDescriptionView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/DispatchWait/EmergencyDescriptionView.swift @@ -14,7 +14,7 @@ final class EmergencyDescriptionView: UIView { label.font = UIFont(weight: .bold, size: 20) label.textAlignment = .left label.textColor = .mainBlack - label.text = "Call 911" + label.text = "call_ins_txt".localized() return label }() @@ -24,7 +24,7 @@ final class EmergencyDescriptionView: UIView { label.textAlignment = .left label.numberOfLines = 4 label.textColor = .mainBlack - label.text = "Calling 911 is the first priority. Ask the people around you to report or perform CPR after reporting. If the report is false, you will be restricted from using the app." + label.text = "call_des_txt".localized() return label }() @@ -67,6 +67,6 @@ final class EmergencyDescriptionView: UIView { private func setUpStyle() { backgroundColor = .white - self.layer.cornerRadius = 8 + self.layer.cornerRadius = 28 } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CallMainViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CallMainViewController.swift index 09e8d48..218c26e 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CallMainViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CallMainViewController.swift @@ -6,6 +6,7 @@ // import Combine +import CombineCocoa import GoogleMaps import UIKit @@ -26,9 +27,15 @@ final class CallMainViewController: UIViewController { let view = TimeCounterView(viewModel: viewModel) return view }() - private let currentLocationNoticeView = CurrentLocationNoticeView() + private let currentLocationNoticeView = CurrentLocationNoticeView(locationInfo: .originLocation) private let callButton = CallCircleView() + private lazy var dispatchEndNoticeView: CustomNoticeView = { + let view = CustomNoticeView(noticeAs: .dispatchComplete) + view.setUpAction(callVC: self, viewModel: viewModel) + return view + }() + private let viewModel: CallViewModel private var cancellables = Set() @@ -43,6 +50,7 @@ final class CallMainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + bind(viewModel: viewModel) setUpConstraints() setUpUserLocation() @@ -50,6 +58,21 @@ final class CallMainViewController: UIViewController { setUpStyle() setUpDelegate() setUpAction() + NotificationCenter.default.addObserver(self, selector: #selector(showCallerPage), name: NSNotification.Name("ShowCallerPage"), object: nil) + + viewModel.$callerListInfo + .receive(on: DispatchQueue.main) + .sink { [weak self] callerListInfo in + guard let callerListInfo = callerListInfo else { return } + if callerListInfo.call_list.count > 0 { + let callId = callerListInfo.call_list[0].cpr_call_id + guard let self = self else { return } + guard let target = callerListInfo.call_list.filter({$0.cpr_call_id == callId}).first else { return } + let callerInfo = CallerInfo(latitude: target.latitude, longitude: target.longitude, cpr_call_id: target.cpr_call_id, full_address: target.full_address, called_at: target.called_at) + let navigationController = UINavigationController(rootViewController: DispatchViewController(userLocation: self.viewModel.getLocation(), callerInfo: callerInfo, viewModel: viewModel)) + present(navigationController, animated: true) + } + }.store(in: &cancellables) } override func viewWillAppear(_ animated: Bool) { @@ -66,7 +89,8 @@ final class CallMainViewController: UIViewController { mapView, timeCounterView, currentLocationNoticeView, - callButton + callButton, + dispatchEndNoticeView ].forEach({ view.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false @@ -83,7 +107,7 @@ final class CallMainViewController: UIViewController { currentLocationNoticeView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: make.space16), currentLocationNoticeView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), currentLocationNoticeView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), - currentLocationNoticeView.heightAnchor.constraint(equalToConstant: 50) + currentLocationNoticeView.heightAnchor.constraint(equalToConstant: 42) ]) NSLayoutConstraint.activate([ @@ -92,6 +116,13 @@ final class CallMainViewController: UIViewController { callButton.widthAnchor.constraint(equalToConstant: 80), callButton.heightAnchor.constraint(equalToConstant: 80) ]) + + NSLayoutConstraint.activate([ + dispatchEndNoticeView.topAnchor.constraint(equalTo: view.topAnchor), + dispatchEndNoticeView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + dispatchEndNoticeView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + dispatchEndNoticeView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) } @@ -104,9 +135,12 @@ final class CallMainViewController: UIViewController { } private func setUpAction() { - let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(didPressCallButton)) - recognizer.minimumPressDuration = 0.0 - callButton.addGestureRecognizer(recognizer) + let longPress = UILongPressGestureRecognizer() + longPress.minimumPressDuration = 0.0 + callButton.addGestureRecognizer(longPress) + longPress.longPressPublisher.sink { [weak self] recognizer in + self?.didPressCallButton(recognizer) + }.store(in: &cancellables) } @@ -134,24 +168,26 @@ final class CallMainViewController: UIViewController { } private func setUpCallerLocation() { - Task { if !callerLocationMarkers.isEmpty { callerLocationMarkers.forEach({ $0.map = nil }) callerLocationMarkers = [] } - guard let callerList = try await self.viewModel.receiveCallerList() else { return } + viewModel.$callerListInfo + .receive(on: DispatchQueue.main) + .sink { [weak self] callerList in + guard let callerList = callerList else { return } + for caller in callerList.call_list { + let coor = CLLocationCoordinate2D(latitude: caller.latitude, longitude: caller.longitude) + let marker = GMSMarker() + marker.title = String(caller.cpr_call_id) + marker.position = CLLocationCoordinate2DMake(coor.latitude, coor.longitude) + marker.map = self?.mapView + self?.callerLocationMarkers.append(marker) + } + }.store(in: &cancellables) - for caller in callerList.call_list { - let coor = CLLocationCoordinate2D(latitude: caller.latitude, longitude: caller.longitude) - print(coor) - let marker = GMSMarker() - marker.title = String(caller.cpr_call_id) - marker.position = CLLocationCoordinate2DMake(coor.latitude, coor.longitude) - marker.map = mapView - callerLocationMarkers.append(marker) - } } } @@ -174,17 +210,9 @@ final class CallMainViewController: UIViewController { output.currentLocationAddress?.sink { address in self.currentLocationNoticeView.setUpLocationLabelText(as: address) }.store(in: &cancellables) - - output.callerList?.sink { list in - print(list) - if list.angel_status == "ACQUIRED" && !list.is_patient { - print(list.call_list) - } - - }.store(in: &cancellables) } - @objc func didPressCallButton(_ sender: UILongPressGestureRecognizer) { + private func didPressCallButton(_ sender: UILongPressGestureRecognizer) { let state = sender.state if state == .began { callButton.progressAnimation() @@ -194,17 +222,52 @@ final class CallMainViewController: UIViewController { timeCounterView.cancelTimeCount() } } + + @objc func showCallerPage(_ notification:Notification) { + self.dismiss(animated: true) + if let userInfo = notification.userInfo { + let type = userInfo["type"] as! String + if type == "1" { + viewModel.receiveCallerList() + viewModel.$callerListInfo + .receive(on: DispatchQueue.main) + .sink { [weak self] callerListInfo in + guard let self = self else { return } + let callId = Int(userInfo["call"] as! String) + guard let target = callerListInfo?.call_list.filter({$0.cpr_call_id == callId}).first else { return } + let callerInfo = CallerInfo(latitude: target.latitude, longitude: target.longitude, cpr_call_id: target.cpr_call_id, full_address: target.full_address, called_at: target.called_at) + let navigationController = UINavigationController(rootViewController: DispatchViewController(userLocation: self.viewModel.getLocation(), callerInfo: callerInfo, viewModel: viewModel)) + self.present(navigationController, animated: true) + }.store(in: &cancellables) + } + } + } } extension CallMainViewController: GMSMapViewDelegate { func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { guard let callId = Int(marker.title ?? "0") else { return false } - guard let target = viewModel.callerList?.value.call_list.filter{$0.cpr_call_id == callId}.first else { return false } + guard let target = viewModel.callerListInfo?.call_list.filter({$0.cpr_call_id == callId}).first else { return false } - let callerInfo = CallerCompactInfo(callerId: target.cpr_call_id, latitude: target.latitude, longitude: target.longitude, callerAddress: target.full_address) - let navigationController = UINavigationController(rootViewController: DispatchViewController(userLocation: viewModel.getLocation(), callerInfo: callerInfo)) - present(navigationController, animated: true, completion: nil) + let callerInfo = CallerInfo(latitude: target.latitude, longitude: target.longitude, cpr_call_id: target.cpr_call_id, full_address: target.full_address, called_at: target.called_at) + let navigationController = UINavigationController(rootViewController: DispatchViewController(userLocation: viewModel.getLocation(), callerInfo: callerInfo, viewModel: viewModel)) + let vc = navigationController.topViewController as? DispatchViewController + vc?.dispatchTimerView.delegate = self + present(navigationController, animated: true, completion: nil) return true } } + +extension CallMainViewController: DispatchTimerViewDelegate { + func noticeAppear(dispatchId: Int) { + dispatchEndNoticeView.setUpDispatchComponent(dispatchId: dispatchId) + dispatchEndNoticeView.noticeAppear() + } +} + +extension CallMainViewController: ReportViewControllerDelegate { + func noticeDisappear() { + dispatchEndNoticeView.noticeHide() + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CurrentLocationNoticeView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CurrentLocationNoticeView.swift index 49ccec8..07f38a5 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CurrentLocationNoticeView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Call/Main/CurrentLocationNoticeView.swift @@ -7,11 +7,16 @@ import UIKit +enum LocationInfo { + case originLocation + case targetLocation +} + class CurrentLocationNoticeView: UIView { private let pinImageView: UIImageView = { let view = UIImageView() - let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .light, scale: .medium) + let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .light, scale: .medium) guard let img = UIImage(systemName: "mappin.circle", withConfiguration: config)?.withTintColor(.mainRed).withRenderingMode(.alwaysOriginal) else { return UIImageView() } view.image = img return view @@ -26,10 +31,10 @@ class CurrentLocationNoticeView: UIView { return label }() - override init(frame: CGRect) { - super.init(frame: frame) + init(locationInfo: LocationInfo) { + super.init(frame: CGRect.zero) - setUpConstraints() + setUpConstraints(locationInfo: locationInfo) setUpStyle() } @@ -37,12 +42,36 @@ class CurrentLocationNoticeView: UIView { fatalError("init(coder:) has not been implemented") } - private func setUpConstraints() { + private func setUpConstraints(locationInfo: LocationInfo) { let make = Constraints.shared + let stackView = UIStackView() + stackView.axis = NSLayoutConstraint.Axis.vertical + stackView.distribution = UIStackView.Distribution.equalSpacing + stackView.alignment = UIStackView.Alignment.leading + stackView.spacing = 0 + + if locationInfo == .targetLocation { + let descriptionLabel = UILabel() + descriptionLabel.font = UIFont(weight: .regular, size: 12) + descriptionLabel.textAlignment = .left + descriptionLabel.textColor = UIColor(rgb: 0x939393) + descriptionLabel.text = "patient_loc_txt".localized() + + stackView.addArrangedSubview(descriptionLabel) + + NSLayoutConstraint.activate([ + descriptionLabel.widthAnchor.constraint(equalToConstant: 180), + descriptionLabel.heightAnchor.constraint(equalToConstant: 14) + ]) + } + + stackView.addArrangedSubview(locationLabel) + + [ pinImageView, - locationLabel + stackView ].forEach({ self.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false @@ -56,9 +85,15 @@ class CurrentLocationNoticeView: UIView { ]) NSLayoutConstraint.activate([ - locationLabel.leadingAnchor.constraint(equalTo: pinImageView.trailingAnchor, constant: make.space8), - locationLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -make.space8), - locationLabel.heightAnchor.constraint(equalToConstant: 28) + stackView.leadingAnchor.constraint(equalTo: pinImageView.trailingAnchor, constant: make.space10), + stackView.centerYAnchor.constraint(equalTo: pinImageView.centerYAnchor), + stackView.widthAnchor.constraint(equalToConstant: 300), + stackView.heightAnchor.constraint(equalToConstant: 38) + ]) + + NSLayoutConstraint.activate([ + locationLabel.widthAnchor.constraint(equalToConstant: 300), + locationLabel.heightAnchor.constraint(equalToConstant: 24) ]) } @@ -66,7 +101,7 @@ class CurrentLocationNoticeView: UIView { backgroundColor = .white self.layer.borderColor = UIColor.mainRed.cgColor self.layer.borderWidth = 2 - self.layer.cornerRadius = 20 + self.layer.cornerRadius = 16 } func setUpLocationLabelText(as str: String) { diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchDescriptionView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchDescriptionView.swift index 276df05..a519173 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchDescriptionView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchDescriptionView.swift @@ -8,7 +8,7 @@ import UIKit enum DispatchDescriptionType: String { - case duration = "Duration" + case startTime = "Start Time" case distance = "Distance" } @@ -22,16 +22,16 @@ class DispatchDescriptionView: UIView { private lazy var titleLabel: UILabel = { let label = UILabel() label.font = UIFont(weight: .regular, size: 14) - label.textAlignment = .center + label.textAlignment = .left label.textColor = .black return label }() private lazy var descriptionLabel: UILabel = { let label = UILabel() - label.font = UIFont(weight: .regular, size: 18) + label.font = UIFont(weight: .bold, size: 24) label.textAlignment = .center - label.textColor = .black + label.textColor = .mainRed label.text = "---" return label }() @@ -46,9 +46,29 @@ class DispatchDescriptionView: UIView { } func setUpConstraints() { - [ + let make = Constraints.shared + + let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel, + ]) + + stackView.axis = NSLayoutConstraint.Axis.horizontal + stackView.alignment = UIStackView.Alignment.center + stackView.spacing = make.space4 + + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 16), + imageView.heightAnchor.constraint(equalToConstant: 16) + ]) + + NSLayoutConstraint.activate([ + titleLabel.heightAnchor.constraint(equalToConstant: 20) + ]) + titleLabel.sizeToFit() + + [ + stackView, descriptionLabel ].forEach({ self.addSubview($0) @@ -57,20 +77,17 @@ class DispatchDescriptionView: UIView { }) NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: self.topAnchor), - imageView.widthAnchor.constraint(equalToConstant: 24), - imageView.heightAnchor.constraint(equalToConstant: 24) - ]) - - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor), - titleLabel.widthAnchor.constraint(equalToConstant: 75), - titleLabel.heightAnchor.constraint(equalToConstant: 20) + stackView.topAnchor.constraint(equalTo: self.topAnchor), + stackView.centerXAnchor.constraint(equalTo: stackView.centerXAnchor), + stackView.heightAnchor.constraint(equalToConstant: 25) ]) + stackView.sizeToFit() + titleLabel.sizeToFit() NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2), - descriptionLabel.widthAnchor.constraint(equalToConstant: 75), + descriptionLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: make.space4), + descriptionLabel.centerXAnchor.constraint(equalTo: stackView.centerXAnchor), + descriptionLabel.widthAnchor.constraint(equalToConstant: 140), descriptionLabel.heightAnchor.constraint(equalToConstant: 25) ]) } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchTimerView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchTimerView.swift index 51622af..82224bf 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchTimerView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchTimerView.swift @@ -6,11 +6,21 @@ // import Combine +import GoogleMaps import UIKit -class DispatchTimerView: UIView { +protocol DispatchTimerViewDelegate: AnyObject { + func noticeAppear(dispatchId: Int) +} - private let calledTime: Date? +class DispatchTimerView: UIView { + + weak var delegate: DispatchTimerViewDelegate? + + private var dispatchId: Int? + + private var calledTime: Date? + private var callerInfo: CallerInfo? private let timeImageView: UIImageView = { let view = UIImageView() @@ -18,21 +28,34 @@ class DispatchTimerView: UIView { return view }() + private let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 16) + label.textAlignment = .left + label.textColor = .mainRed + label.text = "elapsed_time_txt".localized() + return label + }() private lazy var timeLabel: UILabel = { let label = UILabel() label.font = UIFont(weight: .bold, size: 48) label.textColor = .mainRed - label.textAlignment = .right + label.textAlignment = .center label.text = "00:00" return label }() private var timer: Timer.TimerPublisher? + + private var manager = MapManager() + private var viewModel: CallViewModel? private var cancellables = Set() - init(calledTime: Date) { - self.calledTime = calledTime + init(callerInfo: CallerInfo, calledTime: Date, viewModel: CallViewModel) { super.init(frame: CGRect.zero) + self.calledTime = calledTime + self.callerInfo = callerInfo + self.viewModel = viewModel setUpConstraints() setUpStyle() } @@ -43,57 +66,91 @@ class DispatchTimerView: UIView { private func setUpConstraints() { - let timeStackView = UIStackView() - timeStackView.axis = NSLayoutConstraint.Axis.horizontal - timeStackView.distribution = UIStackView.Distribution.equalSpacing + let timeLabelStackView = UIStackView(arrangedSubviews: [ + timeImageView, + descriptionLabel + ]) + + timeLabelStackView.axis = NSLayoutConstraint.Axis.horizontal + timeLabelStackView.alignment = UIStackView.Alignment.center + + timeLabelStackView.spacing = 4 + + NSLayoutConstraint.activate([ + timeImageView.widthAnchor.constraint(equalToConstant: 16), + timeImageView.heightAnchor.constraint(equalToConstant: 16) + ]) + + NSLayoutConstraint.activate([ + descriptionLabel.heightAnchor.constraint(equalToConstant: 24) + ]) + descriptionLabel.sizeToFit() + + let timeStackView = UIStackView() + timeStackView.axis = NSLayoutConstraint.Axis.vertical timeStackView.alignment = UIStackView.Alignment.center timeStackView.spacing = 8 - self.addSubview(timeStackView) timeStackView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - timeStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - timeStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - timeStackView.widthAnchor.constraint(equalToConstant: 182), - timeStackView.heightAnchor.constraint(equalToConstant: 50) - ]) - [ - timeImageView, + timeLabelStackView, timeLabel ].forEach({ - timeStackView.addSubview($0) + timeStackView.addArrangedSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false - $0.centerYAnchor.constraint(equalTo: timeStackView.centerYAnchor).isActive = true }) NSLayoutConstraint.activate([ - timeImageView.leadingAnchor.constraint(equalTo: timeStackView.leadingAnchor), - timeImageView.widthAnchor.constraint(equalToConstant: 50), - timeImageView.heightAnchor.constraint(equalToConstant: 50) + timeLabel.widthAnchor.constraint(equalToConstant: 182), + timeLabel.heightAnchor.constraint(equalToConstant: 50) ]) NSLayoutConstraint.activate([ - timeLabel.trailingAnchor.constraint(equalTo: timeStackView.trailingAnchor), - timeLabel.widthAnchor.constraint(equalToConstant: 182), - timeLabel.heightAnchor.constraint(equalToConstant: 50) + timeStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + timeStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + timeStackView.heightAnchor.constraint(equalToConstant: 80) + ]) + + NSLayoutConstraint.activate([ + timeLabelStackView.centerXAnchor.constraint(equalTo: timeStackView.centerXAnchor), + timeLabelStackView.heightAnchor.constraint(equalToConstant: 24) ]) + timeLabelStackView.sizeToFit() + } private func setUpStyle() { - backgroundColor = .white + backgroundColor = UIColor(rgb: 0xF5F5F5) } - func setTimer() { + func setTimer(startTime: Int) { timer = Timer.publish(every: 1,tolerance: 0.9, on: .main, in: .default) timer? .autoconnect() - .scan(0) { counter, _ in counter + 1 } + .scan(startTime) { counter, _ in counter + 1 } .sink { [self] counter in - if counter == 301 { + if counter > 900 { timer?.connect().cancel() + parentViewController().dismiss(animated: true) } else { + let userLocation = manager.setLocation() + guard let callerInfo = callerInfo else { return } + let distance = calculateDistanceFromCurrentLocation(callerInfo: callerInfo, userLocation: userLocation) + if distance < 20 { + guard let dispatchId = dispatchId else { return } + Task { + guard let viewModel = viewModel else { return } + let isSucceed = try await viewModel.dispatchEnd(dispatchId: dispatchId) + if isSucceed { + } else { + print("CAN'T DISMISS") + } + } + delegate?.noticeAppear(dispatchId: dispatchId) + cancelTimer() + parentViewController().dismiss(animated: true) + } timeLabel.text = counter.numberAsTime() } }.store(in: &cancellables) @@ -103,4 +160,18 @@ class DispatchTimerView: UIView { timer?.connect().cancel() timer = nil } + + func setUpTimerText(startTime: Int) { + timeLabel.text = startTime.numberAsTime() + } + + func setDispatchComponent(dispatchId: Int) { + self.dispatchId = dispatchId + } + + func calculateDistanceFromCurrentLocation(callerInfo: CallerInfo, userLocation: CLLocationCoordinate2D) -> CLLocationDistance { + let callerLocation = CLLocationCoordinate2D(latitude: callerInfo.latitude, longitude: callerInfo.longitude) + let rawDistance = GMSGeometryDistance(userLocation, callerLocation) + return floor(rawDistance) + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchViewController.swift index b50ea59..e522d79 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/DispatchViewController.swift @@ -10,48 +10,40 @@ import CombineCocoa import GoogleMaps import UIKit -struct CallerCompactInfo { - let callerId: Int - let latitude: Double - let longitude: Double - let callerAddress: String -} - final class DispatchViewController: UIViewController { - private let callerLocationNoticeView = CurrentLocationNoticeView() + + private let callerLocationNoticeView = CurrentLocationNoticeView(locationInfo: .targetLocation) private let stackView: UIStackView = { let stackView = UIStackView() stackView.axis = NSLayoutConstraint.Axis.horizontal - stackView.distribution = UIStackView.Distribution.equalSpacing stackView.alignment = UIStackView.Alignment.center stackView.spacing = 12 - stackView.layer.borderColor = UIColor(rgb: 0x938C8C).cgColor + stackView.backgroundColor = UIColor(rgb: 0xF5F5F5) stackView.layer.cornerRadius = 16 - stackView.layer.borderWidth = 1 return stackView }() private let stackViewDecoLine: UIView = { let view = UIView() - view.backgroundColor = .mainLightGray + view.backgroundColor = UIColor(rgb: 0xBCBCBC) return view }() - private let durationView: DispatchDescriptionView = { + private let startTimeView: DispatchDescriptionView = { let view = DispatchDescriptionView() - view.setUpComponent(imageName: "time_check.png", type: .duration) + view.setUpComponent(imageName: "time_black.png", type: .startTime) return view }() private let distanceView: DispatchDescriptionView = { let view = DispatchDescriptionView() - view.setUpComponent(imageName: "map.png", type: .distance) + view.setUpComponent(imageName: "map_black.png", type: .distance) return view }() - private lazy var dispatchTimerView: DispatchTimerView = { - let view = DispatchTimerView(calledTime: Date()) + lazy var dispatchTimerView: DispatchTimerView = { + let view = DispatchTimerView(callerInfo: callerInfo, calledTime: Date(), viewModel: viewModel) view.layer.borderColor = UIColor(rgb: 0x938C8C).cgColor view.layer.cornerRadius = 16 view.layer.borderWidth = 1 @@ -59,37 +51,38 @@ final class DispatchViewController: UIViewController { return view }() + private lazy var dispatchDescriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textAlignment = .center + label.numberOfLines = 2 + label.textColor = UIColor(rgb: 0x454545) + label.text = "dispatch_des_txt".localized() + return label + }() private let dispatchButton: UIButton = { let button = UIButton() button.titleLabel?.font = UIFont(weight: .bold, size: 18) button.setTitleColor(.white, for: .normal) button.backgroundColor = .mainRed button.layer.cornerRadius = 27.5 - button.setTitle("DISPATCH", for: .normal) + button.setTitle("dispatch_tab_t".localized(), for: .normal) return button }() - private let reportLabel: UILabel = { - let label = UILabel() - label.font = UIFont(weight: .regular, size: 14) - label.textColor = .mainBlack - label.textAlignment = .right - label.text = "Wrong Report? Report" - label.isHidden = true - label.isUserInteractionEnabled = true - return label - }() - - private let manager = DispatchManager(service: APIManager()) - private let callerInfo: CallerCompactInfo + private let viewModel: CallViewModel + private let callerInfo: CallerInfo private let userLocation: CLLocationCoordinate2D private var dispatchId: Int? private var isDispatched: Bool = false private var cancellables = Set() - init (userLocation: CLLocationCoordinate2D, callerInfo: CallerCompactInfo) { + var noticeView: CustomNoticeView? + + init (userLocation: CLLocationCoordinate2D, callerInfo: CallerInfo, viewModel: CallViewModel) { self.userLocation = userLocation self.callerInfo = callerInfo + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -99,14 +92,14 @@ final class DispatchViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - + setUpConstraints() setUpStyle() setUpComponent() bind() - setUpAction() setupSheet() - calculateDurationNDistance() + calculateDistance() + calculateTime(dateStr: callerInfo.called_at) } override func viewWillDisappear(_ animated: Bool) { @@ -121,8 +114,8 @@ final class DispatchViewController: UIViewController { callerLocationNoticeView, stackView, dispatchTimerView, + dispatchDescriptionLabel, dispatchButton, - reportLabel ].forEach({ view.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false @@ -137,25 +130,26 @@ final class DispatchViewController: UIViewController { ]) NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: callerLocationNoticeView.bottomAnchor, constant: make.space8), - stackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space8), - stackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space8), + stackView.topAnchor.constraint(equalTo: callerLocationNoticeView.bottomAnchor, constant: make.space16), + stackView.leadingAnchor.constraint(equalTo: callerLocationNoticeView.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: callerLocationNoticeView.trailingAnchor), stackView.heightAnchor.constraint(equalToConstant: 108) ]) NSLayoutConstraint.activate([ - dispatchTimerView.topAnchor.constraint(equalTo: callerLocationNoticeView.bottomAnchor, constant: make.space8), - dispatchTimerView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space8), - dispatchTimerView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space8), + dispatchTimerView.topAnchor.constraint(equalTo: callerLocationNoticeView.bottomAnchor, constant: make.space16), + dispatchTimerView.leadingAnchor.constraint(equalTo: callerLocationNoticeView.leadingAnchor), + dispatchTimerView.trailingAnchor.constraint(equalTo: callerLocationNoticeView.trailingAnchor), dispatchTimerView.heightAnchor.constraint(equalToConstant: 108) ]) + dispatchTimerView.sizeToFit() [ - durationView, + startTimeView, stackViewDecoLine, distanceView, ].forEach({ - stackView.addSubview($0) + stackView.addArrangedSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false $0.centerYAnchor.constraint(equalTo: stackView.centerYAnchor).isActive = true }) @@ -167,15 +161,20 @@ final class DispatchViewController: UIViewController { ]) NSLayoutConstraint.activate([ - durationView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 56), - durationView.widthAnchor.constraint(equalToConstant: 75), - durationView.heightAnchor.constraint(equalToConstant: 70) + startTimeView.widthAnchor.constraint(equalToConstant: 75), + startTimeView.heightAnchor.constraint(equalToConstant: 58) ]) NSLayoutConstraint.activate([ - distanceView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -56), distanceView.widthAnchor.constraint(equalToConstant: 75), - distanceView.heightAnchor.constraint(equalToConstant: 70) + distanceView.heightAnchor.constraint(equalToConstant: 58) + ]) + + NSLayoutConstraint.activate([ + dispatchDescriptionLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: make.space16), + dispatchDescriptionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dispatchDescriptionLabel.widthAnchor.constraint(equalToConstant: 320), + dispatchDescriptionLabel.heightAnchor.constraint(equalToConstant: 40) ]) NSLayoutConstraint.activate([ @@ -184,13 +183,6 @@ final class DispatchViewController: UIViewController { dispatchButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space8), dispatchButton.heightAnchor.constraint(equalToConstant: 55) ]) - - NSLayoutConstraint.activate([ - reportLabel.topAnchor.constraint(equalTo: dispatchButton.bottomAnchor, constant: make.space12), - reportLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space8), - reportLabel.trailingAnchor.constraint(equalTo: dispatchButton.trailingAnchor), - reportLabel.heightAnchor.constraint(equalToConstant: 20) - ]) } private func setUpStyle() { @@ -198,39 +190,30 @@ final class DispatchViewController: UIViewController { } private func setUpComponent() { - callerLocationNoticeView.setUpLocationLabelText(as: callerInfo.callerAddress) + callerLocationNoticeView.setUpLocationLabelText(as: callerInfo.full_address) } private func bind() { dispatchButton.tapPublisher.sink { [self] in if isDispatched { + guard let dispatchId = dispatchId else { return } Task { - guard let dispatchId = dispatchId else { return } - let result = try await manager.dispatchEnd(dispatchId: dispatchId) - if result.success { + let isSucceed = try await viewModel.dispatchEnd(dispatchId: dispatchId) + if isSucceed { + print("DISMISS!!!") dismiss(animated: true) + } else { + print("CAN'T DISMISS") } } } else { - Task { - let result = try await manager.dispatchAccept(cprCallId: callerInfo.callerId) - if result.success { - dispatchId = result.data?.dispatch_id - isModalInPresentation = true - dispatchButton.setTitle("ARRIVED", for: .normal) - stackView.isHidden = true - reportLabel.isHidden = false - timerAppear() - dispatchTimerView.setTimer() - isDispatched = true - } - } + showAlert() + } }.store(in: &cancellables) } private func setupSheet() { - if let sheet = sheetPresentationController { sheet.detents = [.custom { _ in return 300 }] sheet.selectedDetentIdentifier = .medium @@ -244,44 +227,71 @@ final class DispatchViewController: UIViewController { self.dispatchTimerView.isHidden = false }) } - - private func setUpAction() { - let gesture = UITapGestureRecognizer(target: self, action: #selector(didTapReportButton)) - reportLabel.addGestureRecognizer(gesture) - } - - @objc func didTapReportButton() { - guard let dispatchId = dispatchId else { return } - let vc = ReportViewController(dispatchId: dispatchId, manager: manager) - vc.modalPresentationStyle = .fullScreen - present(vc, animated: true) - } } extension DispatchViewController { - func calculateDurationNDistance() { + func calculateDistance() { let callerLocation = CLLocationCoordinate2D(latitude: callerInfo.latitude, longitude: callerInfo.longitude) let rawDistance = GMSGeometryDistance(userLocation, callerLocation) - - let floorDistance = floor(rawDistance) - var duration: Int = 0 + let floorDistance = calculateDistanceFromCurrentLocation() var distanceStr = "" - if floorDistance < 100 { - duration = 1 + if floorDistance < 1000 { + distanceStr = "\(floorDistance)m" } else { - duration = Int(ceil(Float(rawDistance/100))) - if floorDistance < 1000 { - distanceStr = "\(floorDistance)m" - } else { - let distance: Double = rawDistance/1000 - distanceStr = String(format: "%.2f", distance) + "km" - } + let distance: Double = rawDistance/1000 + distanceStr = String(format: "%.2f", distance) + "km" } - - print("RAW: \(rawDistance)") - print("FLOOR: \(floorDistance)") - print(distanceStr) - durationView.setUpDescription(text: "\(duration)m") distanceView.setUpDescription(text: distanceStr) } + + func calculateDistanceFromCurrentLocation() -> CLLocationDistance { + let callerLocation = CLLocationCoordinate2D(latitude: callerInfo.latitude, longitude: callerInfo.longitude) + let rawDistance = GMSGeometryDistance(userLocation, callerLocation) + return floor(rawDistance) + } + + func calculateTime(dateStr: String) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy'-'MM'-'dd' 'HH':'mm':'ss" + guard let tempDate = dateFormatter.date(from: dateStr) else { return } + dateFormatter.dateFormat = "a HH':'mm" + let date = dateFormatter.string(from: tempDate) + startTimeView.setUpDescription(text: date) + } + + func showAlert() { + let alert = UIAlertController(title: "dispatch_ins_txt".localized(), message: "dispatch_alert_txt".localized(), preferredStyle: .alert) + + let confirm = UIAlertAction(title: "dispatch_tab_t".localized(), style: .default, handler: { [weak self] _ in + guard let self = self else { return } + self.acceptDispatch() + }) + + let cancel = UIAlertAction(title: "cancel".localized(), style: .cancel, handler: nil) + [confirm, cancel].forEach { + alert.addAction($0) + } + + present(alert, animated: true, completion: nil) + } + + private func acceptDispatch() { + Task { + let result = try await viewModel.dispatchAccept(cprCallId: callerInfo.cpr_call_id) + if result.0 { + guard let data = result.1 else { return } + let dispatchId = data.dispatch_id + print("########", dispatchId) + isModalInPresentation = true + dispatchButton.isHidden = true + dispatchDescriptionLabel.isHidden = false + stackView.isHidden = true + timerAppear() + dispatchTimerView.setUpTimerText(startTime: data.called_at.elapsedTime()) + dispatchTimerView.setTimer(startTime: data.called_at.elapsedTime()) + isDispatched = true + dispatchTimerView.setDispatchComponent(dispatchId: dispatchId) + } + } + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/ReportViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/ReportViewController.swift index 9b96d6e..5d46659 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/ReportViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Dispatch/ReportViewController.swift @@ -9,8 +9,13 @@ import Combine import CombineCocoa import UIKit +protocol ReportViewControllerDelegate: AnyObject { + func noticeDisappear() +} final class ReportViewController: UIViewController { + weak var delegate: ReportViewControllerDelegate? + private let titleLabel: UILabel = { let label = UILabel() label.font = UIFont(weight: .bold, size: 24) @@ -18,7 +23,7 @@ final class ReportViewController: UIViewController { label.adjustsFontSizeToFitWidth = true label.minimumScaleFactor = 0.5 label.textColor = .mainBlack - label.text = "What are you trying to report?" + label.text = "report_ins_txt".localized() return label }() @@ -27,11 +32,11 @@ final class ReportViewController: UIViewController { label.font = UIFont(weight: .regular, size: 14) label.textAlignment = .left label.textColor = .mainBlack - label.text = "Your report will be treated anonymously." + label.text = "report_des_txt".localized() return label }() - private let placeHolder = "Content*" + private let placeHolder = "report_phdr".localized() private let reportTextView: UITextView = { let view = UITextView() view.layer.cornerRadius = 6 @@ -51,17 +56,17 @@ final class ReportViewController: UIViewController { button.setTitleColor(.white, for: .normal) button.backgroundColor = .mainRed button.layer.cornerRadius = 27.5 - button.setTitle("SUBMIT", for: .normal) + button.setTitle("submit".localized(), for: .normal) return button }() private let dispatchId: Int - private let manager: DispatchManager + private let viewModel: CallViewModel private var cancellables = Set() - init(dispatchId: Int, manager: DispatchManager) { + init(dispatchId: Int, viewModel: CallViewModel) { self.dispatchId = dispatchId - self.manager = manager + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -136,9 +141,10 @@ final class ReportViewController: UIViewController { submitButton.tapPublisher.sink { [self] in Task { let reportInfo = ReportInfo(content: reportTextView.text, dispatch_id: dispatchId) - let result = try await manager.userReport(reportInfo: reportInfo) - if result.success { + let isSucceed = try await viewModel.userReport(reportInfo: reportInfo) + if isSucceed { dismiss(animated: true) + delegate?.noticeDisappear() } } @@ -173,7 +179,7 @@ extension ReportViewController: UITextViewDelegate { textView.textColor = .black } } - + func textViewDidEndEditing(_ textView: UITextView) { if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { textView.text = placeHolder diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/EducationViewModel.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/EducationViewModel.swift index 9cde82c..fc253ac 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/EducationViewModel.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/EducationViewModel.swift @@ -8,6 +8,60 @@ import Combine import UIKit +struct CertificateStatus { + let status: AngelStatus + let leftDay: Int? +} + +enum AngleStatus: String { + case adequate = "Adequate" + case almost = "Almost Adequate" + case notGood = "Not Good" + case bad = "Bad" + + // 팔 각도 + // CORRECT : NON-CORRECT + // 7:3 : 40점 + // 6:4 : 25점 + // 5:5 : 10점 + // 나머지 : 0점 + var score: Int { + switch self { + case .adequate: + return 40 + case .almost: + return 25 + case .notGood: + return 10 + case .bad: + return 0 + } + } + + var description: String { + switch self { + case .adequate: + return "Good job! Very Nice angle!" + case .almost: + return "Almost there. Try again" + case .notGood: + return "Pay more attention to the angle of your arms" + case .bad: + return "You need some more practice" + } + } + + var isSucceed: Bool { + switch self { + case .adequate: + return true + case .almost, .notGood, .bad: + return false + } + } +} + +// Tensorflow 관련 수치 Notation은 추후 Refactoring 시 재검토 예정 enum CompressionRateStatus: String { case tooSlow = "Too Slow" case slow = "Slow" @@ -16,18 +70,18 @@ enum CompressionRateStatus: String { case tooFast = "Too Fast" case wrong = "Wrong" - // 압박 속도 - // 190-250 : 33점 - // 170-270 : 22점 - // 150-290 : 11점 + // 압박 속도: 40% + // 100 - 130 : 40점 + // 80 - 150 : 25점 + // 80 아래 | 150 위 : 10점 var score: Int { switch self { case .adequate: - return 33 + return 40 case .slow, .fast: - return 22 + return 25 case .tooSlow, .tooFast: - return 11 + return 10 case .wrong: return 0 } @@ -49,69 +103,37 @@ enum CompressionRateStatus: String { return "Something went wrong. Try Again" } } -} - -enum AngleStatus: String { - case adequate = "Adequate" - case almost = "Almost Adequate" - case notGood = "Not Good" - case bad = "Bad" - // 팔 각도 - // CORRECT : NON-CORRECT - // 7:3 : 33점 - // 6:4 : 22점 - // 5:5 : 11점 - // 나머지 : 5점 - var score: Int { + var isSucceed: Bool { switch self { case .adequate: - return 33 - case .almost: - return 22 - case .notGood: - return 11 - case .bad: - return 5 + return true + case .tooSlow, .slow, .fast, .tooFast, .wrong: + return false } } - var description: String { - switch self { - case .adequate: - return "Good job! Very Nice angle!" - case .almost: - return "Almost there. Try again" - case .notGood: - return "Pay more attention to the angle of your arms" - case .bad: - return "You need some more practice" - } - } } enum PressDepthStatus: String { case deep = "Deep" case adequate = "Adequate" case shallow = "Slightly Shallow" - case tooShallow = "Too Shallow" case wrong = "Wrong" - // 압박 깊이 - // 30 이상 : 15 - // 18 - 30 : 33 - // 5 - 18 : 15 - // 0 - 5. : 5 + // 압박 깊이 : 20% + // 30 이상 : 10 + // 18 - 30 : 20 + // 5 - 18 : 10 + // 0 - 5. : 0 var score: Int { switch self { case .deep: - return 15 + return 10 case .adequate: - return 33 + return 20 case .shallow: - return 15 - case .tooShallow: - return 5 + return 10 case .wrong: return 0 } @@ -125,25 +147,30 @@ enum PressDepthStatus: String { return "Good job! Very adequate!" case .shallow: return "Press little deeper" - case .tooShallow: - return "It's too shallow. Press deeply" case .wrong: return "Something went wrong. Try Again" } } + + var isSucceed: Bool { + switch self { + case .adequate: + return true + case .deep, .shallow, .wrong: + return false + } + } } -struct CertificateStatus { - let status: AngelStatus - let leftDay: Int? -} - +//func getArmAngleResult() -> (correct: Int, nonCorrect: Int) { +// return (correctAngle, incorrectAngle) +//} enum AngelStatus: Int { case acquired case expired case unacquired - + func certificationImageName(_ isBig: Bool = false) -> String { switch self { case .acquired: @@ -153,31 +180,73 @@ enum AngelStatus: Int { } } - func certificationStatus() -> String { + func getStatus() -> String { switch self { case .acquired: - return "ACQUIRED" + return "acq_status".localized() case .expired: - return "EXPIRED" + return "exp_status".localized() case .unacquired: - return "UNACQUIRED" + return "unacq_status".localized() } } } enum TimerType: Int { - case lecture = 3001 - case posture = 130 + case lecture = 3000 + case posture = 126 case other = 0 } -final class EducationViewModel: AsyncOutputOnlyViewModelType { +enum EducationCourseInfo: String { + case lecture = "course_lec" + case quiz = "course_quiz" + case pose = "course_pose" + + var name: String { + return self.rawValue.localized() + } + + var description: String { + switch self { + case .lecture: + return "course_lec_des".localized() + case .quiz: + return "course_quiz_des".localized() + case .pose: + return "course_pose_des".localized() + } + } + var timeValue: Int { + switch self { + case .lecture: + return 50 + case .quiz: + return 5 + case .pose: + return 3 + } + } +} +struct EducationCourse { + var info: EducationCourseInfo + var courseStatus = CurrentValueSubject(.locked) + + init(course: EducationCourseInfo) { + self.info = course + } +} + +final class EducationViewModel: AsyncOutputOnlyViewModelType { private let eduManager: EducationManager + + @Published private(set) var educationCourse: [EducationCourse] = [ + EducationCourse(course: .lecture), + EducationCourse(course: .quiz), + EducationCourse(course: .pose) + ] - private let eduName: [String] = ["Lecture" , "Quiz", "Pose Practice"] - private let eduDescription: [String] = ["Video lecture for CPR angel certificate", "Let’s check your CPR study", "Posture practice to get CPR angel certificate"] - private var eduStatusArr:[CurrentValueSubject] = [CurrentValueSubject(false), CurrentValueSubject(false), CurrentValueSubject(false)] private var input: Input? private var currentTimerType = TimerType.other @@ -199,9 +268,6 @@ final class EducationViewModel: AsyncOutputOnlyViewModelType { let angelStatus: CurrentValueSubject let progressPercent: CurrentValueSubject let leftDay: CurrentValueSubject - let isLectureCompleted: CurrentValueSubject - let isQuizCompleted: CurrentValueSubject - let isPostureCompleted: CurrentValueSubject } struct Output { @@ -210,18 +276,6 @@ final class EducationViewModel: AsyncOutputOnlyViewModelType { let progressPercent: CurrentValueSubject? } - func educationName() -> [String] { - return eduName - } - - func educationDescription() -> [String] { - return eduDescription - } - - func educationStatus() -> [CurrentValueSubject] { - return eduStatusArr - } - func timeLimit() -> Int { currentTimerType.rawValue } @@ -240,7 +294,7 @@ final class EducationViewModel: AsyncOutputOnlyViewModelType { guard let leftDayNum = input?.leftDay.value else { return CurrentValueSubject(CertificateStatus(status: status, leftDay: nil)) } - + return CurrentValueSubject(CertificateStatus(status: status, leftDay: leftDayNum)) }() @@ -276,19 +330,34 @@ final class EducationViewModel: AsyncOutputOnlyViewModelType { let result = Task { () -> Input? in let eduResult = try await self.eduManager.getEducationProgress() guard let data = eduResult.data else { return nil } - + let progressPercent = Float(data.progress_percent) - let isLectureCompleted = data.is_lecture_completed == 2 - let isQuizCompleted = data.is_quiz_completed == 2 - let isPostureCompleted = data.is_posture_completed == 2 - let isCompleted = [isLectureCompleted, isQuizCompleted, isPostureCompleted] - for i in 0.. Bool { + let result = Task { + let eduResult = try await eduManager.saveQuizResult(score: 100) + let userInfo = try await receiveEducationStatus() + updateInput(data: userInfo) + return eduResult.success + } + return try await result.value + } + func getLecture() async throws -> String? { let result = Task { () -> String? in let eduResult = try await eduManager.getLecture() @@ -330,23 +409,31 @@ final class EducationViewModel: AsyncOutputOnlyViewModelType { func updateInput(data: UserInfo?) { let progressPercent = Float(data?.progress_percent ?? 0) - let isLectureCompleted = data?.is_lecture_completed == 2 - let isQuizCompleted = data?.is_quiz_completed == 2 - let isPostureCompleted = data?.is_posture_completed == 2 - + DispatchQueue.main.async { [weak self] in self?.input?.nickname.send(data?.nickname ?? "") self?.input?.angelStatus.send(data?.angel_status ?? 0) self?.input?.progressPercent.send(progressPercent) self?.input?.leftDay.send(data?.days_left_until_expiration ?? nil) - self?.input?.isLectureCompleted.send(isLectureCompleted) - self?.input?.isQuizCompleted.send(isQuizCompleted) - self?.input?.isPostureCompleted.send(isPostureCompleted) - let isCompleted = [isLectureCompleted, isQuizCompleted, isPostureCompleted] - guard let count = self?.eduStatusArr.count else { return } - for i in 0..() - - init() { - super.init(frame: CGRect.zero) - setUpConstraints() - setUpStyle() - setUpAction() - - Task { - setUpAddreessList() - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setUpConstraints() { - self.addSubview(noticeView) - noticeView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - noticeView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - noticeView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - noticeView.widthAnchor.constraint(equalToConstant: 313), - noticeView.heightAnchor.constraint(equalToConstant: 308) - ]) - - [ - titleLabel, - mainAddressTextField, - subAddressTextField, - confirmButton - ].forEach({ - noticeView.addSubview($0) - $0.translatesAutoresizingMaskIntoConstraints = false - }) - - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: noticeView.topAnchor, constant: 26), - titleLabel.centerXAnchor.constraint(equalTo: noticeView.centerXAnchor), - titleLabel.widthAnchor.constraint(equalTo: noticeView.widthAnchor), - titleLabel.heightAnchor.constraint(equalToConstant: 48) - ]) - - NSLayoutConstraint.activate([ - mainAddressTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 32), - mainAddressTextField.centerXAnchor.constraint(equalTo: noticeView.centerXAnchor), - mainAddressTextField.widthAnchor.constraint(equalTo: noticeView.widthAnchor), - mainAddressTextField.heightAnchor.constraint(equalToConstant: 34) - ]) - - NSLayoutConstraint.activate([ - subAddressTextField.topAnchor.constraint(equalTo: mainAddressTextField.bottomAnchor, constant: 24), - subAddressTextField.centerXAnchor.constraint(equalTo: noticeView.centerXAnchor), - subAddressTextField.widthAnchor.constraint(equalTo: noticeView.widthAnchor), - subAddressTextField.heightAnchor.constraint(equalToConstant: 34) - ]) - - NSLayoutConstraint.activate([ - confirmButton.bottomAnchor.constraint(equalTo: noticeView.bottomAnchor, constant: -26), - confirmButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), - confirmButton.widthAnchor.constraint(equalToConstant: 206), - confirmButton.heightAnchor.constraint(equalToConstant: 44) - ]) - - } - - private func setUpStyle() { - self.alpha = 0.0 - self.backgroundColor = UIColor(rgb: 0x7B7B7B).withAlphaComponent(0.45) - } - - private func setUpAddreessList() { - Task { - let result = try await addressManager.getAddressList() - if result.success == true { - guard let list = result.data else { return } - addressList = list - setUpPickerView() - } - - } - } - - private func setUpPickerView() { - let mainPickerView = UIPickerView() - mainPickerView.layer.name = "mainPickerView" - let subPickerView = UIPickerView() - subPickerView.layer.name = "subPickerView" - - [mainPickerView, subPickerView].forEach({ - $0.delegate = self - $0.dataSource = self - }) - - [mainAddressTextField, subAddressTextField].forEach({ - let toolBar = UIToolbar() - toolBar.sizeToFit() - let button = UIBarButtonItem(title: "완료", style: .plain, target: self, action: #selector(self.didSelectButtonTapped)) - toolBar.setItems([button], animated: true) - toolBar.isUserInteractionEnabled = true - $0.inputAccessoryView = toolBar - }) - - mainAddressTextField.inputView = mainPickerView - subAddressTextField.inputView = subPickerView - } - - private func setUpAction() { - confirmButton.tapPublisher.sink { - Task { - guard let id = self.addressId else { - return } - try await self.addressManager.setUserAddress(id: id) - self.noticeDisappear() - } - }.store(in: &cancellables) - } - - @objc func didSelectButtonTapped() { - mainAddressTextField.endEditing(true) - subAddressTextField.endEditing(true) - } - - func noticeAppear() { - self.superview?.isUserInteractionEnabled = false - UIView.animate(withDuration: appearAnimDuration, animations: { - self.alpha = 1.0 - }, completion: { _ in - self.superview?.isUserInteractionEnabled = true - }) - } - - func noticeDisappear() { - UIView.animate(withDuration: appearAnimDuration/2, delay: 0, animations: { - self.alpha = 0.0 - }, completion: { [weak self] _ in - self?.superview?.isUserInteractionEnabled = true - self?.removeFromSuperview() - }) - } -} - -extension AddressSettingView: UIPickerViewDelegate, UIPickerViewDataSource { - func numberOfComponents(in pickerView: UIPickerView) -> Int { - if pickerView.layer.name == "mainPickerView" { - return 1 - } else if pickerView.layer.name == "subPickerView" { - return 1 - } - return 1 - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - if pickerView.layer.name == "mainPickerView" { - return addressList.count - } else if pickerView.layer.name == "subPickerView" { - guard let index = mainAddressIndex else { return 0 } - return addressList[index].gugun_list.count - } - return 0 - } - - func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - if pickerView.layer.name == "mainPickerView" { - return addressList[row].sido - } else if pickerView.layer.name == "subPickerView" { - guard let index = mainAddressIndex else { return "" } - return addressList[index].gugun_list[row].gugun - } - return nil - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - if pickerView.layer.name == "mainPickerView" { - mainAddressTextField.text = addressList[row].sido - mainAddressTextField.textColor = .mainBlack - if addressList[row].sido == "세종특별자치시" { - addressId = addressList[row].gugun_list[0].id - subAddressTextField.isHidden = true - } else { - addressId = nil - subAddressTextField.text = "구/군" - subAddressTextField.textColor = UIColor(rgb: 0xC1C1C1) - subAddressTextField.isHidden = false - } - mainAddressIndex = row - } else if pickerView.layer.name == "subPickerView" { - guard let index = mainAddressIndex else { return } - subAddressTextField.text = addressList[index].gugun_list[row].gugun - subAddressTextField.textColor = .mainBlack - addressId = addressList[index].gugun_list[row].id - } - } -} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/CertificateStatusView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/CertificateStatusView.swift index 4264c4b..b6ba805 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/CertificateStatusView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/CertificateStatusView.swift @@ -5,6 +5,7 @@ // Created by 황정현 on 2023/03/09. // +import Foundation import UIKit final class CertificateStatusView: UIView { @@ -93,8 +94,6 @@ final class CertificateStatusView: UIView { private func setUpStyle() { self.layer.cornerRadius = 16 - self.layer.borderColor = UIColor.mainRed.cgColor - self.layer.borderWidth = 1 self.backgroundColor = .mainWhite } @@ -105,11 +104,11 @@ final class CertificateStatusView: UIView { var statusString: String if let leftDayString = leftDay { - statusString = "\(status.certificationStatus()) (D-\(leftDayString))" + statusString = "\(status.getStatus()) (D-\(leftDayString))" } else { - statusString = status.certificationStatus() + statusString = status.getStatus() } - let certificateStatusString = "Certificate Status: " + let certificateStatusString = "cert_status_ann_txt".localized() let customFont = UIFont(weight: .bold, size: 14) let attributes: [NSAttributedString.Key: Any] = [ .font: customFont, @@ -123,6 +122,7 @@ final class CertificateStatusView: UIView { } func setUpGreetingLabel(nickname: String) { - greetingLabel.text = "Hi \(nickname)" + let localizedStr = String(format: "%@_greeting_txt".localized(), nickname) + greetingLabel.text = localizedStr } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationCollectionViewCell.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationCollectionViewCell.swift index 4ef30ce..7afd11d 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationCollectionViewCell.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationCollectionViewCell.swift @@ -7,29 +7,156 @@ import UIKit +enum CourseStatus { + case completed + case now + case locked + + var cellColor: UIColor { + switch self { + case .completed: + return UIColor(rgb: 0xF4EFEA) + case .now: + return .mainRed + case .locked: + return UIColor(rgb: 0xDDDDDD) + } + } + + var defaultMainTextColor: UIColor { + switch self { + case .completed: + return UIColor(rgb: 0x333333) + case .now: + return UIColor(rgb: 0xFFFFFF) + case .locked: + return UIColor(rgb: 0x828282) + } + } + + var defaultSubTextColor: UIColor { + switch self { + case .completed: + return UIColor(rgb: 0x525252) + case .now: + return UIColor(rgb: 0xFFFFFF) + case .locked: + return UIColor(rgb: 0x999999) + } + } + + var timeNoticeLabelColor: UIColor { + switch self { + case .completed: + return UIColor(rgb: 0x828282) + case .now: + return UIColor(rgb: 0xFBDDDE) + case .locked: + return UIColor(rgb: 0xAAAAAA) + } + } + + var completeNoticeLabelColor: UIColor { + switch self { + case .completed: + return .mainRed + case .now: + return UIColor(rgb: 0x525252) + case .locked: + return UIColor(rgb: 0xAAAAAA) + } + } + + var completeNoticeLabelText: String { + switch self { + case .completed: + return "completed".localized() + + case .now: + return "completed_not".localized() + case .locked: + return "open_not".localized() + } + } + + var isCheckIconVisible: Bool { + switch self { + case .completed: + return true + case .now, .locked: + return false + } + } + + var isCourseNumberVisible: Bool { + switch self { + case .completed: + return false + case .now, .locked: + return true + } + } + + var courseNumberLabelColor: UIColor { + switch self { + case .completed, .now: + return .white + case .locked: + return UIColor(rgb: 0x828282) + } + } + + var courseViewColor: UIColor { + switch self { + case .completed, .now: + return .mainRed + case .locked: + return UIColor(rgb: 0xF4EFEA) + } + } +} + final class EducationCollectionViewCell: UICollectionViewCell { static let identifier = "EducationCollectionViewCell" private lazy var educationNameLabel: UILabel = { let label = UILabel() - label.font = UIFont(weight: .bold, size: 16) - label.textColor = .mainBlack + label.font = UIFont(weight: .bold, size: 18) + label.textAlignment = .left return label }() + + private let checkImageView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "check") + return view + }() + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textAlignment = .left + return label + }() + + private lazy var timeNoticeLabel: UILabel = { let label = UILabel() label.font = UIFont(weight: .regular, size: 12) - label.textColor = .mainBlack + label.textAlignment = .left return label }() + private lazy var statusLabel: UILabel = { let label = UILabel() - label.font = UIFont(weight: .bold, size: 16) - label.textColor = .mainRed + label.font = UIFont(weight: .bold, size: 12) + label.textAlignment = .center + label.backgroundColor = .white + label.layer.cornerRadius = 8 + label.clipsToBounds = true return label }() - + override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() @@ -41,11 +168,12 @@ final class EducationCollectionViewCell: UICollectionViewCell { } private func setUpConstraints() { - let make = Constraints.shared [ educationNameLabel, + checkImageView, + timeNoticeLabel, descriptionLabel, statusLabel ].forEach({ @@ -54,43 +182,59 @@ final class EducationCollectionViewCell: UICollectionViewCell { }) NSLayoutConstraint.activate([ - educationNameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: make.space10), + educationNameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: make.space16), educationNameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: make.space20), - educationNameLabel.widthAnchor.constraint(equalToConstant: 160), - educationNameLabel.heightAnchor.constraint(equalToConstant: 18), - + educationNameLabel.heightAnchor.constraint(equalToConstant: 26), ]) + educationNameLabel.sizeToFit() NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: educationNameLabel.bottomAnchor, constant: make.space2), + checkImageView.leadingAnchor.constraint(equalTo: educationNameLabel.trailingAnchor, constant: make.space8), + checkImageView.centerYAnchor.constraint(equalTo: educationNameLabel.centerYAnchor), + checkImageView.widthAnchor.constraint(equalToConstant: 16), + checkImageView.heightAnchor.constraint(equalToConstant: 16), + ]) + + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: educationNameLabel.bottomAnchor), descriptionLabel.leadingAnchor.constraint(equalTo: educationNameLabel.leadingAnchor), - educationNameLabel.widthAnchor.constraint(equalToConstant: 160), - educationNameLabel.heightAnchor.constraint(equalToConstant: 18) ]) NSLayoutConstraint.activate([ - statusLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -make.space18), - statusLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -make.space14) + statusLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -make.space16), + statusLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -make.space16), + statusLabel.widthAnchor.constraint(equalToConstant: 110), + statusLabel.heightAnchor.constraint(equalToConstant: 24) + + ]) + + NSLayoutConstraint.activate([ + timeNoticeLabel.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor), + timeNoticeLabel.leadingAnchor.constraint(equalTo: educationNameLabel.leadingAnchor), + timeNoticeLabel.widthAnchor.constraint(equalToConstant: 160), + timeNoticeLabel.heightAnchor.constraint(equalToConstant: 18) ]) } private func setUpStyle() { - self.layer.cornerRadius = 20 + self.layer.cornerRadius = 16 } - func setUpEducationNameLabel(as str: String) { - educationNameLabel.text = str - } - - func setUpDescriptionLabel(as str: String) { - descriptionLabel.text = str - } - - func setUpStatus(isCompleted: Bool) { - statusLabel.text = isCompleted == true ? "Completed" : "Not Completed" - statusLabel.textColor = isCompleted == true ? .mainRed : .mainDarkGray - self.backgroundColor = isCompleted == true ? .mainLightRed : .mainLightGray + func setUpComponent(timeValue: Int, status: CourseStatus) { + print("STATUS?", status) + self.layer.backgroundColor = status.cellColor.cgColor + educationNameLabel.textColor = status.defaultMainTextColor + descriptionLabel.textColor = status.defaultSubTextColor + checkImageView.isHidden = !status.isCheckIconVisible + timeNoticeLabel.textColor = status.timeNoticeLabelColor + timeNoticeLabel.text = String(format: "taken_%dtime_des_txt".localized(), timeValue) + statusLabel.textColor = status.completeNoticeLabelColor + statusLabel.text = status.completeNoticeLabelText } + func setUpLabelText(name: String, description: String) { + educationNameLabel.text = name + descriptionLabel.text = description + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationMainViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationMainViewController.swift index 6835a4c..1dd2384 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationMainViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationMainViewController.swift @@ -9,13 +9,24 @@ import Combine import UIKit protocol EducationMainViewControllerDelegate: AnyObject { - func updateUserEducationStatus() + func updateUserEducationStatus(isPassed: Bool?) } final class EducationMainViewController: UIViewController { private var certificateStatusView = CertificateStatusView() + + private let annotationLabel: UILabel = { + let label = UILabel() + let color = UIColor(rgb: 0x767676) + label.font = UIFont(weight: .regular, size: 12) + label.textColor = color + label.text = "angel_progress_ann_txt".localized() + return label + }() + private let progressView = EducationProgressView() + let educationCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) private let viewModel: EducationViewModel @@ -24,7 +35,6 @@ final class EducationMainViewController: UIViewController { private weak var delegate: EducationMainViewControllerDelegate? private lazy var noticeView = CustomNoticeView(noticeAs: .certificate) - private lazy var addressSettingView = AddressSettingView() init(viewModel: EducationViewModel) { self.viewModel = viewModel @@ -41,17 +51,16 @@ final class EducationMainViewController: UIViewController { setUpConstraints() setUpStyle() bind(to: viewModel) - noticeView.setCertificateNotice() } private func setUpConstraints() { let safeArea = view.safeAreaLayoutGuide let make = Constraints.shared [ + annotationLabel, certificateStatusView, progressView, educationCollectionView, - addressSettingView, noticeView ].forEach({ view.addSubview($0) @@ -66,26 +75,26 @@ final class EducationMainViewController: UIViewController { ]) NSLayoutConstraint.activate([ - progressView.topAnchor.constraint(equalTo: certificateStatusView.bottomAnchor, constant: make.space6), + annotationLabel.topAnchor.constraint(equalTo: certificateStatusView.bottomAnchor, constant: make.space16), + annotationLabel.leadingAnchor.constraint(equalTo: certificateStatusView.leadingAnchor), + annotationLabel.widthAnchor.constraint(equalToConstant: 200), + annotationLabel.heightAnchor.constraint(equalToConstant: 14) + ]) + + NSLayoutConstraint.activate([ + progressView.topAnchor.constraint(equalTo: annotationLabel.bottomAnchor, constant: 28), progressView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), progressView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), progressView.heightAnchor.constraint(equalToConstant: 40) ]) NSLayoutConstraint.activate([ - educationCollectionView.topAnchor.constraint(equalTo: progressView.bottomAnchor), + educationCollectionView.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 32), educationCollectionView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), educationCollectionView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor), educationCollectionView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor) ]) - - NSLayoutConstraint.activate([ - addressSettingView.topAnchor.constraint(equalTo: view.topAnchor), - addressSettingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - addressSettingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - addressSettingView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - + NSLayoutConstraint.activate([ noticeView.topAnchor.constraint(equalTo: view.topAnchor), noticeView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -97,7 +106,7 @@ final class EducationMainViewController: UIViewController { private func setUpStyle() { guard let navBar = self.navigationController?.navigationBar else { return } navBar.prefersLargeTitles = true - navBar.topItem?.title = "Education" + navBar.topItem?.title = "edu_tab_t".localized() navBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.mainRed] self.navigationController?.navigationBar.prefersLargeTitles = true } @@ -114,15 +123,11 @@ final class EducationMainViewController: UIViewController { output.certificateStatus?.sink { status in self.certificateStatusView.setUpStatus(as: status.status, leftDay: status.leftDay) - if status.status == .acquired{ + if status.status == .acquired { if UserDefaultsManager.isCertificateNotice == false { self.noticeView.noticeAppear() UserDefaultsManager.isCertificateNotice = true } - if UserDefaultsManager.isAddressSet == false { - self.addressSettingView.noticeAppear() - UserDefaultsManager.isAddressSet = true - } } }.store(in: &cancellables) @@ -130,9 +135,12 @@ final class EducationMainViewController: UIViewController { self.certificateStatusView.setUpGreetingLabel(nickname: nickname) }.store(in: &cancellables) - output.progressPercent?.sink { progress in - self.progressView.setUpProgress(as: progress) - }.store(in: &cancellables) + viewModel.$educationCourse + .receive(on: DispatchQueue.main) + .sink { educationCourse in + let courseStatus = educationCourse.map({ $0.courseStatus.value}) + self.progressView.setUpComponent(status: courseStatus) + }.store(in: &cancellables) DispatchQueue.main.async { self.setUpCollectionView() @@ -144,23 +152,28 @@ final class EducationMainViewController: UIViewController { extension EducationMainViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return viewModel.educationName().count + return viewModel.educationCourse.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EducationCollectionViewCell", for: indexPath) as! EducationCollectionViewCell - cell.setUpEducationNameLabel(as: viewModel.educationName()[indexPath.row]) - cell.setUpDescriptionLabel(as: viewModel.educationDescription()[indexPath.row]) - cell.setUpStatus(isCompleted: viewModel.educationStatus()[indexPath.row].value) + let course = viewModel.educationCourse[indexPath.row] + cell.setUpLabelText(name: course.info.name, description: course.info.description) + + viewModel.$educationCourse + .receive(on: DispatchQueue.main) + .sink { educationCourse in + cell.setUpComponent(timeValue: educationCourse[indexPath.row].info.timeValue, status: educationCourse[indexPath.row].courseStatus.value) + }.store(in: &cancellables) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let index = indexPath.row - let isCompleted = index != 0 ? viewModel.educationStatus()[index - 1].value : true - if isCompleted == true { + let isCompleted = index != 0 ? viewModel.educationCourse[index - 1].courseStatus.value : .completed + if isCompleted == .completed { navigateTo(index: index) } else { view.showToastMessage(type: .education) @@ -198,7 +211,7 @@ extension EducationMainViewController: UICollectionViewDelegateFlowLayout { vc = LectureViewController(viewModel: viewModel) navigationController?.pushViewController(vc, animated: true) } else if index == 1 { - let temp = EducationQuizViewController() + let temp = EducationQuizViewController(eduViewModel: viewModel) temp.delegate = self vc = UINavigationController(rootViewController: temp) vc.modalPresentationStyle = .overFullScreen @@ -211,14 +224,18 @@ extension EducationMainViewController: UICollectionViewDelegateFlowLayout { } extension EducationMainViewController: EducationMainViewControllerDelegate { - func updateUserEducationStatus() { + + func updateUserEducationStatus(isPassed: Bool?) { Task { - let userInfo = try await viewModel.receiveEducationStatus() - viewModel.updateInput(data: userInfo) - - DispatchQueue.main.async { [weak self] in - self?.educationCollectionView.reloadData() - self?.dismiss(animated: true) + if let isPassed = isPassed { + if isPassed { + _ = try await viewModel.saveQuizResult() + } + DispatchQueue.main.async { [weak self] in + self?.progressView.layoutIfNeeded() + self?.educationCollectionView.reloadData() + self?.dismiss(animated: true) + } } } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationProgressView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationProgressView.swift index 73b9eaa..a61904c 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationProgressView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Main/EducationProgressView.swift @@ -9,33 +9,28 @@ import UIKit final class EducationProgressView: UIView { - private let annotationLabel: UILabel = { - let label = UILabel() - let color = UIColor(rgb: 0x767676) - label.font = UIFont(weight: .regular, size: 11) - label.textColor = color - label.text = "CPR Angel Certification Progress" - return label + private lazy var lectureStatusView: StatusCircleView = { + let view = StatusCircleView() + return view + }() + + private lazy var quizStatusView: StatusCircleView = { + let view = StatusCircleView() + return view }() - private let infoButton: UIButton = { - let button = UIButton() - let color = UIColor(rgb: 0x767676) - let config = UIImage.SymbolConfiguration(pointSize: 10, weight: .light, scale: .medium) - guard let img = UIImage(systemName: "info.circle", withConfiguration: config)?.withTintColor(color).withRenderingMode(.alwaysOriginal) else { return UIButton() } - button.setImage(img, for: .normal) - return button + private lazy var posePracticeStatusView: StatusCircleView = { + let view = StatusCircleView() + return view }() - private lazy var progressView: UIProgressView = { - let view = UIProgressView() - view.trackTintColor = .mainLightRed.withAlphaComponent(0.05) - view.progressTintColor = .mainRed - view.layer.borderColor = UIColor.mainRed.cgColor - view.layer.borderWidth = 1 - view.layer.cornerRadius = 8 - view.layer.sublayers![1].cornerRadius = 8 - view.clipsToBounds = true + private lazy var lecToQuizLine: UIView = { + let view = UIView() + return view + }() + + private lazy var quizToPoseLine: UIView = { + let view = UIView() return view }() @@ -50,40 +45,69 @@ final class EducationProgressView: UIView { } private func setUpConstraints() { - let make = Constraints.shared [ - annotationLabel, - infoButton, - progressView + lectureStatusView, + lecToQuizLine, + quizStatusView, + posePracticeStatusView, + quizToPoseLine ].forEach({ self.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false + $0.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + }) + + [ + lectureStatusView, + quizStatusView, + posePracticeStatusView + ].forEach({ + $0.widthAnchor.constraint(equalToConstant: 40).isActive = true + $0.heightAnchor.constraint(equalToConstant: 40).isActive = true + $0.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true }) + + NSLayoutConstraint.activate([ + lectureStatusView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + ]) + NSLayoutConstraint.activate([ - annotationLabel.topAnchor.constraint(equalTo: self.topAnchor), - annotationLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: make.space6), - annotationLabel.widthAnchor.constraint(equalToConstant: 168), - annotationLabel.heightAnchor.constraint(equalToConstant: 14) + quizStatusView.centerXAnchor.constraint(equalTo: self.centerXAnchor), ]) NSLayoutConstraint.activate([ - infoButton.leadingAnchor.constraint(equalTo: annotationLabel.trailingAnchor, constant: make.space4), - infoButton.centerYAnchor.constraint(equalTo: annotationLabel.centerYAnchor), - infoButton.widthAnchor.constraint(equalToConstant: 10), - infoButton.heightAnchor.constraint(equalToConstant: 10) + posePracticeStatusView.trailingAnchor.constraint(equalTo: self.trailingAnchor), ]) NSLayoutConstraint.activate([ - progressView.topAnchor.constraint(equalTo: annotationLabel.bottomAnchor, constant: make.space2), - progressView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: make.space6), - progressView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - progressView.heightAnchor.constraint(equalToConstant: 16) + lecToQuizLine.heightAnchor.constraint(equalToConstant: 2), + lecToQuizLine.leadingAnchor.constraint(equalTo: lectureStatusView.trailingAnchor), + lecToQuizLine.trailingAnchor.constraint(equalTo: quizStatusView.leadingAnchor) + ]) + + NSLayoutConstraint.activate([ + quizToPoseLine.heightAnchor.constraint(equalToConstant: 2), + quizToPoseLine.leadingAnchor.constraint(equalTo: quizStatusView.trailingAnchor), + quizToPoseLine.trailingAnchor.constraint(equalTo: posePracticeStatusView.leadingAnchor) ]) } - func setUpProgress(as value: Float) { - progressView.progress = value + func setUpComponent(status: [CourseStatus]) { + let statusViewArr = [ + lectureStatusView, + quizStatusView, + posePracticeStatusView + ] + + for i in 0.. Strokes? { - var strokes = Strokes(dots: [], lines: []) - // MARK: Visualization of detection result - var bodyPartToDotMap: [BodyPart: CGPoint] = [:] - for (index, part) in BodyPart.allCases.enumerated() { - if part == .rightAnkle { - + + /// List of lines connecting each part to be visualized. + private static let lines = [ + (from: BodyPart.leftWrist, to: BodyPart.leftElbow), + (from: BodyPart.leftElbow, to: BodyPart.leftShoulder), + (from: BodyPart.leftShoulder, to: BodyPart.rightShoulder), + (from: BodyPart.rightShoulder, to: BodyPart.rightElbow), + (from: BodyPart.rightElbow, to: BodyPart.rightWrist), + (from: BodyPart.leftShoulder, to: BodyPart.leftHip), + (from: BodyPart.leftHip, to: BodyPart.rightHip), + (from: BodyPart.rightHip, to: BodyPart.rightShoulder), + (from: BodyPart.leftHip, to: BodyPart.leftKnee), + (from: BodyPart.leftKnee, to: BodyPart.leftAnkle), + (from: BodyPart.rightHip, to: BodyPart.rightKnee), + (from: BodyPart.rightKnee, to: BodyPart.rightAnkle), + ] + + /// CGContext to draw the detection result. + var context: CGContext! + + /// Draw the detected keypoints on top of the input image. + /// + /// - Parameters: + /// - image: The input image. + /// - person: Keypoints of the person detected (i.e. output of a pose estimation model) + func draw(at image: UIImage, person: Person) { + if context == nil { + UIGraphicsBeginImageContext(image.size) + guard let context = UIGraphicsGetCurrentContext() else { + fatalError("set current context faild") + } + self.context = context } - let position = CGPoint( - x: person.keyPoints[index].coordinate.x, - y: person.keyPoints[index].coordinate.y) - bodyPartToDotMap[part] = position - strokes.dots.append(position) + guard let strokes = strokes(from: person) else { return } + + if flag == true { + measureCprRate(person: person) + measureElbowDegree(person: person) + } + + image.draw(at: .zero) + context.setLineWidth(Config.dot.radius) + context.setStrokeColor(UIColor.blue.cgColor) + context.strokePath() + guard let newImage = UIGraphicsGetImageFromCurrentImageContext() else { fatalError() } + self.image = newImage } - - do { - try strokes.lines = CameraOverlayView.lines.map { map throws -> Line in - guard let from = bodyPartToDotMap[map.from] else { - throw VisualizationError.missingBodyPart(of: map.from) - } - guard let to = bodyPartToDotMap[map.to] else { - throw VisualizationError.missingBodyPart(of: map.to) + + /// Draw the dots (i.e. keypoints). + /// + /// - Parameters: + /// - context: The context to be drawn on. + /// - dots: The list of dots to be drawn. + private func drawDots(at context: CGContext, dots: [CGPoint]) { + for dot in dots { + let dotRect = CGRect( + x: dot.x - Config.dot.radius / 2, y: dot.y - Config.dot.radius / 2, + width: Config.dot.radius, height: Config.dot.radius) + let path = CGPath( + roundedRect: dotRect, cornerWidth: Config.dot.radius, cornerHeight: Config.dot.radius, + transform: nil) + context.addPath(path) } - return Line(from: from, to: to) - } - } catch VisualizationError.missingBodyPart(let missingPart) { - os_log("Visualization error: %s is missing.", type: .error, missingPart.rawValue) - return nil - } catch { - os_log("Visualization error: %s", type: .error, error.localizedDescription) - return nil } - return strokes - } - private func measureCprScore(person: Person) { - var xShoulder: CGFloat = 0 - var yShoulder: CGFloat = 0 - var xElbow: CGFloat = 0 - var yElbow: CGFloat = 0 - var xWrist: CGFloat = 0 - var yWrist: CGFloat = 0 - - // person이 갖고 있는 관절 데이터들에서 어깨, 팔꿈치, 손목 데이터 추출 (현재 임시로 왼쪽 관절만 추출한 상태) - person.keyPoints.forEach( { point in - if point.bodyPart == .leftShoulder { - xShoulder = point.coordinate.x - yShoulder = point.coordinate.y - } else if point.bodyPart == .leftElbow { - xElbow = point.coordinate.x - yElbow = point.coordinate.y - } else if point.bodyPart == .leftWrist { - xWrist = point.coordinate.x - yWrist = point.coordinate.y + /// Generate a list of strokes to draw in order to visualize the pose estimation result. + /// + /// - Parameters: + /// - person: The detected person (i.e. output of a pose estimation model). + private func strokes(from person: Person) -> Strokes? { + var strokes = Strokes(dots: [], lines: []) + // MARK: Visualization of detection result + var bodyPartToDotMap: [BodyPart: CGPoint] = [:] + for (index, part) in BodyPart.allCases.enumerated() { + if part == .rightAnkle { + } - }) - - // 일직선 판별 - var isCorrect = xShoulder - xElbow < 10 && xElbow - xWrist < 10 - if (isCorrect) { - correct += 1 - } else { - nonCorrect += 1 + let position = CGPoint( + x: person.keyPoints[index].coordinate.x, + y: person.keyPoints[index].coordinate.y) + bodyPartToDotMap[part] = position + strokes.dots.append(position) } - // 손목의 높이가 상승 곡선에서 꼭짓점을 찍고 하강하는 경우 - if (increased && beforeWrist > yWrist + 1) { - increased = false - maxHeight = yWrist + do { + try strokes.lines = CameraOverlayView.lines.map { map throws -> Line in + guard let from = bodyPartToDotMap[map.from] else { + throw VisualizationError.missingBodyPart(of: map.from) + } + guard let to = bodyPartToDotMap[map.to] else { + throw VisualizationError.missingBodyPart(of: map.to) + } + return Line(from: from, to: to) + } + } catch VisualizationError.missingBodyPart(let missingPart) { + os_log("Visualization error: %s is missing.", type: .error, missingPart.rawValue) + return nil + } catch { + os_log("Visualization error: %s", type: .error, error.localizedDescription) + return nil } - // 손목의 높이가 하강 곡선에서 꼭짓점을 찍고 상승하는 경우 - else if (!increased && beforeWrist < yWrist - 1) { - increased = true - minHeight = yWrist + return strokes + } + + private func measureCprRate(person: Person) { + var wrist: CGPoint! + + // Calculate for person with accuracy greater than 0.4 + if person.score > 0.4 { + for point in person.keyPoints { + switch point.bodyPart { + case .leftWrist: + wrist = point.coordinate + default: + break + } + } + print(wrist.y) + + pressCount = wristList.count - // wristList에 ${손목의 최대 높이 - 손목의 최소 높이}를 저장 + print("최소 평균 \(avgMinHeight)") + print("최대 평균 \(avgMaxHeight)") - let num = maxHeight > minHeight ? maxHeight - minHeight : minHeight - maxHeight - wristList.append(num) - print(wristList.last) + // If the wrist height is decreasing after an increasing curve + if increased && beforeWrist > wrist.y + 1 { + // Verify if it is an above-average value + avgMaxHeight = (avgMaxHeight * CGFloat(pressCount) + wrist.y) / CGFloat(pressCount + 1) + print("고점 이상값 \(avgMaxHeight - wrist.y)") + if abs(avgMaxHeight - wrist.y) < 50 { + // If it is not considered an outlier, register the peak + increased = false + maxHeight = beforeWrist + print("평균 \(avgMaxHeight) 고점 \(maxHeight)") + } + } + + // If the wrist height is increasing after a decreasing curve + else if !increased && beforeWrist < wrist.y - 1 { + // Verify if it is an above-average value + avgMinHeight = (avgMinHeight * CGFloat(pressCount) + wrist.y) / CGFloat(pressCount + 1) + print("저점 이상값 \(avgMinHeight - wrist.y)") + if abs(avgMinHeight - wrist.y) < 50 { + // If it is not considered an outlier, register the valley + increased = true + minHeight = beforeWrist + print("평균 \(avgMinHeight) 저점 \(minHeight)") + + // Register depth + let depth = maxHeight - minHeight + if depth > 0 { + print("깊이: \(depth)") + wristList.append(depth) + } + + print("개수 \(wristList.count)") + print("\(wristList.last)") + } + } - // wristList에 저장된 깊이 값으로 CPR 깊이가 적절한지 확인한다. - // wristList에 저장된 값의 개수로 CPR 속도(2분 동안 CPR한 횟수)가 적절한지 확인한다. - // 가슴압박 속도는 분당 100~120회, 깊이는 5~6㎝로 빠르고 깊게 30회 압박 - // 2분 -> 200~240회 : 추후 1분당 평균 내는것도 나쁘지 않을듯 + beforeWrist = wrist.y } - - beforeWrist = yWrist } - func getCompressionTotalCount() -> Int { - return wristList.count + private func measureElbowDegree(person: Person) { + // Extract shoulder, elbow, and wrist data from the person's joint data (currently only extracting left joint as an example) + var shoulder: CGPoint! + var elbow: CGPoint! + var wrist: CGPoint! + + for point in person.keyPoints { + switch point.bodyPart { + case .leftShoulder: + shoulder = point.coordinate + case .leftElbow: + elbow = point.coordinate + case .leftWrist: + wrist = point.coordinate + default: + break + } + } + + let isCorrect = shoulder.x - elbow.x < 20 && elbow.x - wrist.x < 20 + if isCorrect { + correctAngle += 1 + } else { + incorrectAngle += 1 + } } - func getArmAngleRate() -> (correct: Int, nonCorrect: Int) { - return (correct, nonCorrect) + + func getCprRateResult() -> Int { + return pressCount / 2 } - func getAveragePressDepth() -> CGFloat { - let total = wristList.reduce(0){$0 + $1} - let len = CGFloat(wristList.count) - return total/len + func getArmAngleResult() -> (correct: Int, nonCorrect: Int) { + return (correctAngle, incorrectAngle) } + func measureIsPreparing(person: Person) -> Bool { + var shoulderLeft: CGPoint! + var shoulderRight: CGPoint! + + var elbowLeft: CGPoint! + var elbowRight: CGPoint! + + var wristLeft: CGPoint! + var wristRight: CGPoint! + + var hipLeft: CGPoint! + var hipRight: CGPoint! + + var kneeLeft: CGPoint! + var kneeRight: CGPoint! + + var ankleLeft: CGPoint! + var ankleRight: CGPoint! + + for point in person.keyPoints { + switch point.bodyPart { + case .leftShoulder: + shoulderLeft = point.coordinate + case .rightShoulder: + shoulderRight = point.coordinate + case .leftElbow: + elbowLeft = point.coordinate + case .rightElbow: + elbowRight = point.coordinate + case .leftWrist: + wristLeft = point.coordinate + case .rightWrist: + wristRight = point.coordinate + case .leftHip: + hipLeft = point.coordinate + case .rightHip: + hipRight = point.coordinate + case .leftKnee: + kneeLeft = point.coordinate + case .rightKnee: + kneeRight = point.coordinate + case .leftAnkle: + ankleLeft = point.coordinate + case .rightAnkle: + ankleRight = point.coordinate + default: + break + } + } + + let value: CGFloat = 25 + + let isElbowLeftVertical = abs(shoulderLeft.x - elbowLeft.x) < value && abs(elbowLeft.x - wristLeft.x) < value + && wristLeft.y > elbowLeft.y && elbowLeft.y > shoulderLeft.y + let isElbowRightVertical = abs(shoulderRight.x - elbowRight.x) < value && abs(elbowRight.x - wristRight.x) < value + && wristRight.y > elbowRight.y && elbowRight.y > shoulderRight.y + + let isBodyLeftVertical = shoulderLeft.x < hipLeft.x && shoulderLeft.y < hipLeft.y + let isBodyRightVertical = shoulderRight.x < hipRight.x && shoulderRight.y < hipRight.y + + let isBodyLeftSeated = hipLeft.x > kneeLeft.x && kneeLeft.x < ankleLeft.x && hipLeft.x < ankleLeft.x + && hipLeft.y < kneeLeft.y && hipLeft.y < ankleLeft.y && abs(ankleLeft.y - kneeLeft.y) < value + let isBodyRightSeated = hipRight.x > kneeRight.x && kneeRight.x < ankleRight.x && hipRight.x < ankleRight.x + && hipRight.y < kneeRight.y && hipRight.y < ankleRight.y && abs(ankleRight.y - kneeRight.y) < value + + let isElbowVertical = isElbowLeftVertical && isElbowRightVertical + let isBodyVertical = isBodyLeftVertical && isBodyRightVertical + let isBodySeated = isBodyLeftSeated && isBodyRightSeated + + if !isElbowVertical || !isBodyVertical || !isBodySeated { + return false + } + + return true + } + + func getCprDepthResult() -> CGFloat { + pressCount = wristList.count + var min: CGFloat = CGFloat.greatestFiniteMagnitude + var max: CGFloat = 0 + var depth: CGFloat = 0 + for w in wristList { + if w < min { + min = w + } else if w > max { + max = w + } + depth += w + } + return (depth - min - max) / CGFloat(pressCount - 2) + } } /// The strokes to be drawn in order to visualize a pose estimation result. fileprivate struct Strokes { - var dots: [CGPoint] - var lines: [Line] + var dots: [CGPoint] + var lines: [Line] } /// A straight line. fileprivate struct Line { - let from: CGPoint - let to: CGPoint + let from: CGPoint + let to: CGPoint } fileprivate enum VisualizationError: Error { - case missingBodyPart(of: BodyPart) + case missingBodyPart(of: BodyPart) } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/EvaluationResultView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/EvaluationResultView.swift index d2eff42..0588856 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/EvaluationResultView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/EvaluationResultView.swift @@ -8,17 +8,36 @@ import UIKit final class EvaluationResultView: UIView { - private let evaluationTargetImageView = UIImageView() - private let titleLabel = UILabel() - private let resultLabel = UILabel() - private let descriptionLabel = UILabel() + private let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 16) + label.textColor = .mainWhite + label.textAlignment = .center + return label + }() + + private let descriptionImageView = UIImageView() + private let resultLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 14) + label.textColor = .mainWhite + label.textAlignment = .center + return label + }() + private let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textColor = .mainWhite + label.textAlignment = .center + label.numberOfLines = 2 + return label + }() override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() - setUpStyle() } required init?(coder: NSCoder) { @@ -28,79 +47,90 @@ final class EvaluationResultView: UIView { private func setUpConstraints() { let make = Constraints.shared - [ + let titleStackView = UIStackView(arrangedSubviews: [ evaluationTargetImageView, - titleLabel, + titleLabel + ]) + titleStackView.axis = NSLayoutConstraint.Axis.horizontal + titleStackView.distribution = .equalSpacing + titleStackView.alignment = .center + titleStackView.spacing = make.space8 + + let stackView = UIStackView(arrangedSubviews: [ + titleStackView, + descriptionImageView, + resultLabel, + descriptionLabel + ]) + stackView.axis = NSLayoutConstraint.Axis.vertical + stackView.distribution = .equalSpacing + stackView.alignment = UIStackView.Alignment.center + stackView.spacing = make.space8 + + self.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor) + ]) + + [ + titleStackView, + descriptionImageView, resultLabel, descriptionLabel ].forEach({ - self.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false }) NSLayoutConstraint.activate([ - evaluationTargetImageView.topAnchor.constraint(equalTo: self.topAnchor, constant: make.space12), - evaluationTargetImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: make.space16), + titleStackView.heightAnchor.constraint(equalToConstant: 28) + ]) + + [ + evaluationTargetImageView, + titleLabel + ].forEach({ + $0.translatesAutoresizingMaskIntoConstraints = false + }) + + + NSLayoutConstraint.activate([ evaluationTargetImageView.widthAnchor.constraint(equalToConstant: 28), evaluationTargetImageView.heightAnchor.constraint(equalToConstant: 28) ]) NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: evaluationTargetImageView.trailingAnchor, constant: make.space4), - titleLabel.centerYAnchor.constraint(equalTo: evaluationTargetImageView.centerYAnchor), - titleLabel.widthAnchor.constraint(equalToConstant: 180), - titleLabel.heightAnchor.constraint(equalToConstant: 35) + descriptionImageView.widthAnchor.constraint(equalToConstant: 48), + descriptionImageView.heightAnchor.constraint(equalToConstant: 48) ]) NSLayoutConstraint.activate([ - resultLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: make.space24), - resultLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - resultLabel.widthAnchor.constraint(equalToConstant: 145), - resultLabel.heightAnchor.constraint(equalToConstant: 24) + resultLabel.widthAnchor.constraint(equalToConstant: 200), + resultLabel.heightAnchor.constraint(equalToConstant: 21) ]) NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: resultLabel.bottomAnchor), - descriptionLabel.leadingAnchor.constraint(equalTo: resultLabel.leadingAnchor), descriptionLabel.widthAnchor.constraint(equalToConstant: 180), - descriptionLabel.heightAnchor.constraint(equalToConstant: 24) + descriptionLabel.heightAnchor.constraint(equalToConstant: 40) ]) } - private func setUpStyle() { - self.layer.cornerRadius = 16 - self.layer.borderColor = UIColor.mainRed.cgColor - self.layer.borderWidth = 1 - self.backgroundColor = .mainLightRed.withAlphaComponent(0.05) - - [ - titleLabel, resultLabel, descriptionLabel - ].forEach({ - $0.textColor = .mainBlack - $0.textAlignment = .left - }) - titleLabel.font = UIFont(weight: .bold, size: 20) - resultLabel.font = UIFont(weight: .bold, size: 14) - descriptionLabel.font = UIFont(weight: .regular, size: 14) - - titleLabel.adjustsFontSizeToFitWidth = true - titleLabel.minimumScaleFactor = 0.5 - - descriptionLabel.adjustsFontSizeToFitWidth = true - descriptionLabel.minimumScaleFactor = 0.5 - descriptionLabel.numberOfLines = 3 - } - func setImage(imgName systemName: String) { let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .regular, scale: .medium) - evaluationTargetImageView.image = UIImage(systemName: systemName, withConfiguration: config) - + evaluationTargetImageView.image = UIImage(systemName: systemName, withConfiguration: config)?.withTintColor(.mainWhite, renderingMode: .alwaysOriginal) } func setTitle(title: String) { titleLabel.text = title } + func setResultImageView(isSuccess: Bool) { + let image = isSuccess ? UIImage(named: "check_badge.png") : UIImage(named: "x_mark.png") + descriptionImageView.image = image + } + func setResultLabelText(as text: String) { resultLabel.text = text } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PoseAvailabilityCheckView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PoseAvailabilityCheckView.swift new file mode 100644 index 0000000..e07862f --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PoseAvailabilityCheckView.swift @@ -0,0 +1,74 @@ +// +// PoseAvailabilityCheckView.swift +// CPR2U +// +// Created by 황정현 on 2023/05/24. +// + +import UIKit + +final class PoseAvailabilityCheckView: UIView { + + private let guidelineImageView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "pose_guideline") + return view + }() + + private let instructionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 42) + label.textAlignment = .center + label.numberOfLines = 1 + label.textColor = .mainRed + label.text = "pe_ins_txt".localized() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setUpConstraints() + setUpStyle() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func setUpConstraints() { + let make = Constraints.shared + + [ + guidelineImageView, + instructionLabel + ].forEach({ + self.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + }) + + NSLayoutConstraint.activate([ + guidelineImageView.topAnchor.constraint(equalTo: self.topAnchor), + guidelineImageView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + guidelineImageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + guidelineImageView.widthAnchor.constraint(equalTo: self.heightAnchor) + ]) + + NSLayoutConstraint.activate([ + instructionLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: make.space16), + instructionLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -make.space16), + instructionLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + instructionLabel.heightAnchor.constraint(equalToConstant: 80) + ]) + } + + private func setUpStyle() { + self.backgroundColor = .white.withAlphaComponent(0.5) + } + + func fadeOut() { + UIView.animate(withDuration: 0.3, animations: { + self.alpha = 0.0 + }) + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeCountDownView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeCountDownView.swift new file mode 100644 index 0000000..0fe6d8e --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeCountDownView.swift @@ -0,0 +1,96 @@ +// +// PosePracticeCountDownView.swift +// CPR2U +// +// Created by 황정현 on 2023/05/24. +// + +import UIKit + +final class PosePracticeCountDownView: UIView { + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 24) + label.textAlignment = .center + label.numberOfLines = 1 + label.textColor = .mainRed + label.text = "pe_count_des_txt".localized() + return label + }() + + private lazy var timeLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 98) + label.textColor = .mainRed + label.shadowColor = UIColor(rgb: 0xB50000) + label.textAlignment = .center + label.text = "3" + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setUpConstriants() + self.alpha = 0.0 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpConstriants() { + let make = Constraints.shared + + let stackView = UIStackView() + stackView.axis = NSLayoutConstraint.Axis.vertical + stackView.distribution = UIStackView.Distribution.equalSpacing + stackView.alignment = UIStackView.Alignment.center + stackView.spacing = make.space8 + + self.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + stackView.widthAnchor.constraint(equalTo: self.widthAnchor), + stackView.heightAnchor.constraint(equalToConstant: 150), + ]) + + [ + descriptionLabel, + timeLabel + ].forEach({ + stackView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + }) + + NSLayoutConstraint.activate([ + descriptionLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor), + descriptionLabel.heightAnchor.constraint(equalToConstant: 42), + ]) + + NSLayoutConstraint.activate([ + timeLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: make.space8), + timeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor), + timeLabel.heightAnchor.constraint(equalToConstant: 100) + ]) + } + + func changeTimerValue(to value: Int) { + timeLabel.text = "\(3 - value)" + } + + func fadeIn() { + UIView.animate(withDuration: 0.3, animations: { + self.alpha = 1.0 + }) + } + + func fadeOut() { + UIView.animate(withDuration: 0.3, animations: { + self.alpha = 0.0 + }) + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeResultViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeResultViewController.swift index 4206672..37abad9 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeResultViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeResultViewController.swift @@ -10,39 +10,93 @@ import CombineCocoa import UIKit final class PosePracticeResultViewController: UIViewController { + + private lazy var pageControl: UIPageControl = { + let pageControl = UIPageControl() + pageControl.numberOfPages = 2 + pageControl.currentPage = 0 + pageControl.pageIndicatorTintColor = .mainLightGray + pageControl.currentPageIndicatorTintColor = .black + return pageControl + }() + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + let landscapeWidth = UIScreen.main.bounds.height + let landscapeHeight = UIScreen.main.bounds.width + scrollView.frame = CGRect(x: 0, y: 0, width: landscapeWidth, height: landscapeHeight) + scrollView.isPagingEnabled = true + scrollView.showsHorizontalScrollIndicator = false + scrollView.contentInsetAdjustmentBehavior = .never + return scrollView + }() + + private let resultLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 28) + label.textAlignment = .left + label.textColor = .mainWhite + label.text = "your_result_txt".localized() + return label + }() + + private lazy var scoreStackView: UIStackView = { + let view = UIStackView() + view.axis = NSLayoutConstraint.Axis.horizontal + view.distribution = UIStackView.Distribution.equalSpacing + view.alignment = UIStackView.Alignment.center + view.spacing = 100 + return view + }() + + private lazy var scoreLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 72) + label.textAlignment = .center + label.textColor = .mainWhite + return label + }() + + private lazy var scoreDescriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 24) + label.textAlignment = .left + label.numberOfLines = 2 + label.textColor = .mainWhite + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.7 + return label + }() + + private lazy var resultStackView: UIStackView = { + let view = UIStackView() + view.axis = NSLayoutConstraint.Axis.horizontal + view.distribution = UIStackView.Distribution.fillEqually + view.spacing = 110 + return view + }() + private let compressRateResultView: EvaluationResultView = { let view = EvaluationResultView() view.setImage(imgName: "ruler") + view.setResultImageView(isSuccess: false) view.setTitle(title: "Compression Rate") - view.setResultLabelText(as: "0.5 per 1 time") - view.setDescriptionLabelText(as: "It’s too fast. Little bit Slower") return view }() private let pressDepthResultView: EvaluationResultView = { let view = EvaluationResultView() view.setImage(imgName: "ruler") + view.setResultImageView(isSuccess: true) view.setTitle(title: "Press Depth") - view.setResultLabelText(as: "Slightly shallow") - view.setDescriptionLabelText(as: "Press little deeper") - return view - }() - - private let handLocationResultView: EvaluationResultView = { - let view = EvaluationResultView() - view.setImage(imgName: "ruler") - view.setTitle(title: "Hand Location") - view.setResultLabelText(as: "Adequate") - view.setDescriptionLabelText(as: "Nice Location!") return view }() private let armAngleResultView: EvaluationResultView = { let view = EvaluationResultView() view.setImage(imgName: "ruler") + view.setResultImageView(isSuccess: true) view.setTitle(title: "Arm Angle") - view.setResultLabelText(as: "Adequate") - view.setDescriptionLabelText(as: "Nice Angle!") return view }() @@ -50,17 +104,22 @@ final class PosePracticeResultViewController: UIViewController { private let quitButton: UIButton = { let button = UIButton() - button.backgroundColor = .mainRed - button.layer.cornerRadius = 19 - button.titleLabel?.font = UIFont(weight: .bold, size: 17) - button.setTitleColor(.mainWhite, for: .normal) - button.setTitle("QUIT", for: .normal) + button.backgroundColor = .mainWhite + button.layer.cornerRadius = 24 + button.titleLabel?.font = UIFont(weight: .bold, size: 20) + button.setTitleColor(.mainBlack, for: .normal) + button.setTitle("quit".localized(), for: .normal) return button }() + private lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + private let viewModel: EducationViewModel private var cancellables = Set() - private var score: Int = 0 init(viewModel: EducationViewModel) { @@ -77,8 +136,11 @@ final class PosePracticeResultViewController: UIViewController { setUpConstraints() setUpStyle() + setUpDelegate() bind(viewModel: viewModel) setUpText() + + setUpOrientation(as: .landscape) // TEST CODE } private func setUpConstraints() { @@ -86,69 +148,126 @@ final class PosePracticeResultViewController: UIViewController { let make = Constraints.shared [ - compressRateResultView, - pressDepthResultView, - handLocationResultView, - armAngleResultView, - finalResultView, + resultLabel, + pageControl, + scrollView, quitButton ].forEach({ view.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false }) - let evaluationResultViewArr = [compressRateResultView, pressDepthResultView, handLocationResultView, armAngleResultView,] + [ + scoreStackView, + resultStackView, + ].forEach({ + scrollView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + }) + + NSLayoutConstraint.activate([ + resultLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: make.space24), + resultLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), + resultLabel.widthAnchor.constraint(equalToConstant: 200), + resultLabel.heightAnchor.constraint(equalToConstant: 36) + ]) + + scrollView.contentSize.height = scrollView.frame.height + + + NSLayoutConstraint.activate([ + scoreStackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 95), + scoreStackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerXAnchor, constant: -95), + scoreStackView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor, constant: -make.space8), + scoreStackView.heightAnchor.constraint(equalToConstant: 108) + ]) + + NSLayoutConstraint.activate([ + resultStackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerXAnchor, constant: 38), + resultStackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -38), + resultStackView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor, constant: -make.space24), + resultStackView.heightAnchor.constraint(equalToConstant: 128) + ]) + + [ + scoreLabel, + scoreDescriptionLabel + ].forEach({ + scoreStackView.addArrangedSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + }) + + NSLayoutConstraint.activate([ + scoreLabel.widthAnchor.constraint(equalToConstant: 158), + scoreLabel.heightAnchor.constraint(equalToConstant: 108) + ]) + + NSLayoutConstraint.activate([ + scoreDescriptionLabel.widthAnchor.constraint(equalToConstant: 396), + scoreDescriptionLabel.heightAnchor.constraint(equalToConstant: 72) + ]) + + let evaluationResultViewArr = [ + armAngleResultView, + compressRateResultView, + pressDepthResultView + ] evaluationResultViewArr.forEach({ - $0.widthAnchor.constraint(equalToConstant: 255).isActive = true - $0.heightAnchor.constraint(equalToConstant: 150).isActive = true + resultStackView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.widthAnchor.constraint(equalToConstant: 196).isActive = true + $0.heightAnchor.constraint(equalToConstant: 128).isActive = true }) NSLayoutConstraint.activate([ - compressRateResultView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: make.space24), - compressRateResultView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16) + armAngleResultView.leadingAnchor.constraint(equalTo: resultStackView.leadingAnchor) ]) NSLayoutConstraint.activate([ - pressDepthResultView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -make.space24), - pressDepthResultView.leadingAnchor.constraint(equalTo: compressRateResultView.leadingAnchor), + compressRateResultView.centerXAnchor.constraint(equalTo: resultStackView.centerXAnchor) ]) NSLayoutConstraint.activate([ - handLocationResultView.topAnchor.constraint(equalTo: compressRateResultView.topAnchor), - handLocationResultView.leadingAnchor.constraint(equalTo: compressRateResultView.trailingAnchor, constant: make.space16) + pressDepthResultView.trailingAnchor.constraint(equalTo: resultStackView.trailingAnchor) ]) NSLayoutConstraint.activate([ - armAngleResultView.bottomAnchor.constraint(equalTo: pressDepthResultView.bottomAnchor), - armAngleResultView.leadingAnchor.constraint(equalTo: compressRateResultView.trailingAnchor, constant: make.space16) + pageControl.bottomAnchor.constraint(equalTo: quitButton.topAnchor, constant: -make.space12), + pageControl.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor), + pageControl.widthAnchor.constraint(equalTo: safeArea.widthAnchor), + pageControl.heightAnchor.constraint(equalToConstant: 12) ]) NSLayoutConstraint.activate([ - quitButton.bottomAnchor.constraint(equalTo: armAngleResultView.bottomAnchor), - quitButton.leadingAnchor.constraint(equalTo: armAngleResultView.trailingAnchor, constant: make.space16), - quitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -make.space16), - quitButton.heightAnchor.constraint(equalToConstant: 45) + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) NSLayoutConstraint.activate([ - finalResultView.topAnchor.constraint(equalTo: handLocationResultView.topAnchor), - finalResultView.bottomAnchor.constraint(equalTo: quitButton.topAnchor, constant: -make.space12), - finalResultView.leadingAnchor.constraint(equalTo: quitButton.leadingAnchor), - finalResultView.trailingAnchor.constraint(equalTo: quitButton.trailingAnchor) - + quitButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -make.space16), + quitButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + quitButton.widthAnchor.constraint(equalToConstant: 206), + quitButton.heightAnchor.constraint(equalToConstant: 48) ]) } private func setUpStyle() { - view.backgroundColor = .white + view.backgroundColor = .mainRed } + private func setUpDelegate() { + scrollView.delegate = self + } + private func bind(viewModel: EducationViewModel) { quitButton.tapPublisher.sink { [weak self] _ in self?.setUpOrientation(as: .portrait) Task { - try await viewModel.savePosturePracticeResult(score: self?.score ?? 0) +// _ = try await viewModel.savePosturePracticeResult(score: self?.score ?? 0) + _ = try await viewModel.savePosturePracticeResult(score: 95) let rootVC = TabBarViewController(0) await self?.view.window?.setRootViewController(rootVC) } @@ -159,11 +278,66 @@ final class PosePracticeResultViewController: UIViewController { let result = viewModel.judgePostureResult() compressRateResultView.setResultLabelText(as: result.compResult.rawValue) compressRateResultView.setDescriptionLabelText(as: result.compResult.description) + armAngleResultView.setResultImageView(isSuccess: result.compResult.isSucceed) + armAngleResultView.setResultLabelText(as: result.angleResult.rawValue) armAngleResultView.setDescriptionLabelText(as: result.angleResult.description) + armAngleResultView.setResultImageView(isSuccess: result.angleResult.isSucceed) + pressDepthResultView.setResultLabelText(as: result.pressDepth.rawValue) pressDepthResultView.setDescriptionLabelText(as: result.pressDepth.description) + pressDepthResultView.setResultImageView(isSuccess: result.pressDepth.isSucceed) + score = result.compResult.score + result.angleResult.score + result.pressDepth.score + 1 - finalResultView.setUpScore(score: score) + setUpScore(score: score) + view.layoutIfNeeded() + } + + func setUpScore(score: Int) { + let scoreStr = "\(score)/100" + scoreLabel.text = scoreStr + scoreLabel.attributedText = arrangeScoreText(text: scoreLabel.text ?? "") + + if score >= 80 { + scoreDescriptionLabel.text = "pe_pass_txt".localized() + } else { + scoreDescriptionLabel.text = "pe_fail_txt".localized() + } + } + + private func arrangeScoreText(text: String) -> NSAttributedString { + var range: Int = 0 + let attributedString = NSMutableAttributedString(string: text) + if let rangeS = text.range(of: "/") { + range = text.distance(from: text.startIndex, to: rangeS.lowerBound) + } + + let realScoreAttributes: [NSAttributedString.Key: Any] = [ + .font : UIFont(weight: .bold, size: 72) ?? UIFont(), + .foregroundColor : UIColor.mainWhite + ] + + let defaultScoreAttributes: [NSAttributedString.Key: Any] = [ + .font : UIFont(weight: .bold, size: 32) ?? UIFont(), + .foregroundColor : UIColor.mainLightRed + ] + + attributedString.addAttributes(realScoreAttributes, range: NSRange(location: 0, length: range)) + attributedString.addAttributes(defaultScoreAttributes, range: NSRange(location: range, length: attributedString.length - range)) + + return attributedString + } + + private func setPageControlSelectedPage(currentPage:Int) { + pageControl.currentPage = currentPage } + +} + +extension PosePracticeResultViewController: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let value = scrollView.contentOffset.x/scrollView.frame.size.width + setPageControlSelectedPage(currentPage: Int(round(value))) + } + } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeViewController.swift index ca65cc6..8de3f30 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PosePracticeViewController.swift @@ -23,6 +23,7 @@ enum Constants { final class PosePracticeViewController: UIViewController { + var person: Person? private let timeImageView: UIImageView = { let view = UIImageView() let config = UIImage.SymbolConfiguration(pointSize: 26, weight: .regular, scale: .medium) @@ -57,10 +58,12 @@ final class PosePracticeViewController: UIViewController { button.layer.cornerRadius = 19 button.titleLabel?.font = UIFont(weight: .bold, size: 17) button.setTitleColor(.mainWhite, for: .normal) - button.setTitle("QUIT", for: .normal) + button.setTitle("quit".localized(), for: .normal) return button }() + private let poseAvailabilityCheckView = PoseAvailabilityCheckView() + private let posePracticeCountDownView = PosePracticeCountDownView() private lazy var overlayView = CameraOverlayView() // MARK: Pose estimation model configs @@ -83,6 +86,8 @@ final class PosePracticeViewController: UIViewController { private var cancellables = Set() private var audioPlayer: AVAudioPlayer! + private let assumePostureCountSec = 3 + init(viewModel: EducationViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) @@ -99,8 +104,8 @@ final class PosePracticeViewController: UIViewController { setUpConstraints() updateModel() configCameraCapture() - setTimer() - playSound() + setPoseAssumeTimer() + setUpAudioPlayer() setUpAction() } @@ -128,6 +133,8 @@ final class PosePracticeViewController: UIViewController { [ overlayView, + poseAvailabilityCheckView, + posePracticeCountDownView, timeImageView, timeLabel, soundImageView, @@ -173,6 +180,20 @@ final class PosePracticeViewController: UIViewController { quitButton.heightAnchor.constraint(equalToConstant: 38) ]) + NSLayoutConstraint.activate([ + poseAvailabilityCheckView.topAnchor.constraint(equalTo: view.topAnchor), + poseAvailabilityCheckView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + poseAvailabilityCheckView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + poseAvailabilityCheckView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + NSLayoutConstraint.activate([ + posePracticeCountDownView.topAnchor.constraint(equalTo: view.topAnchor), + posePracticeCountDownView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + posePracticeCountDownView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + posePracticeCountDownView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + NSLayoutConstraint.activate([ overlayView.topAnchor.constraint(equalTo: view.topAnchor), overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -200,50 +221,76 @@ final class PosePracticeViewController: UIViewController { } } + private func setPoseAssumeTimer() { + viewModel.timer = Timer.publish(every: 0.1, on: .current, in: .common) + viewModel.timer + .autoconnect() + .scan(0) { counter, _ in counter + 1 } + .sink { [weak self] counter in + guard let self = self, let person = person else { return } + if (self.overlayView.measureIsPreparing(person: person)) { + self.poseAvailabilityCheckView.fadeOut() + self.posePracticeCountDownView.fadeIn() + self.viewModel.timer.connect().cancel() + setTimer() + playSound() + } + }.store(in: &cancellables) + } + private func setTimer() { let count = viewModel.timeLimit() viewModel.timer = Timer.publish(every: 1, on: .current, in: .common) viewModel.timer .autoconnect() - .scan(0) { counter, _ in counter + 1 } + .scan(1) { counter, _ in counter + 1 } .sink { [self] counter in - if counter > 5 { - timeLabel.text = (count - counter - 5).numberAsTime() - if counter == count - 5 { + if counter > assumePostureCountSec + 1 { + timeLabel.text = (count - counter - assumePostureCountSec).numberAsTime() + if counter == count - assumePostureCountSec { cameraFeedManager.stopRunning() - viewModel.setPostureResult(compCount: overlayView.getCompressionTotalCount(), armAngleCount: overlayView.getArmAngleRate(), pressDepth: overlayView.getAveragePressDepth()) + viewModel.setPostureResult(compCount: overlayView.getCprRateResult(), armAngleCount: overlayView.getArmAngleResult(), pressDepth: overlayView.getCprDepthResult()) Task { usleep(1000000) - audioPlayer.stop() + audioPlayer?.stop() let vc = PosePracticeResultViewController(viewModel: viewModel) vc.modalPresentationStyle = .overFullScreen self.present(vc, animated: true) } viewModel.timer.connect().cancel() + } + } else if counter == assumePostureCountSec + 1 { + timeLabel.text = (count - counter - assumePostureCountSec).numberAsTime() + posePracticeCountDownView.fadeOut() + overlayView.flag = true + } else { + posePracticeCountDownView.changeTimerValue(to: counter - 1) } - } }.store(in: &cancellables) } private func setUpAction() { soundSwitch.isOnPublisher.sink { isOn in - self.audioPlayer.volume = isOn ? 1 : 0 + self.audioPlayer?.volume = isOn ? 1 : 0 }.store(in: &cancellables) quitButton.tapPublisher.sink { [weak self] in - self?.audioPlayer.stop() + self?.audioPlayer?.stop() self?.setUpOrientation(as: .portrait) self?.dismiss(animated: true) }.store(in: &cancellables) } - private func playSound() { + private func setUpAudioPlayer() { guard let url = Bundle.main.url(forResource: "CPR_Posture_Sound", withExtension: "mp3") else { return } do { audioPlayer = try AVAudioPlayer(contentsOf: url) } catch (let error) { print(error) } + } + + private func playSound() { audioPlayer?.play() } @@ -277,8 +324,8 @@ extension PosePracticeViewController: CameraFeedManagerDelegate { self.overlayView.image = image return } - self.overlayView.draw(at: image, person: result) + self.person = result } } catch { os_log("Error running pose estimation.", type: .error) diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PracticeExplainViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PracticeExplainViewController.swift index cd547d0..3a8de22 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PracticeExplainViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Pose/PracticeExplainViewController.swift @@ -9,16 +9,30 @@ import Combine import CombineCocoa import UIKit +struct PracticeInfo { + let imageName: String + let title: String + let description: String +} + final class PracticeExplainViewController: UIViewController { - // temp: 추후 설명용 이미지가 삽입될 영역 - private let imageList: [String] = ["onboarding1.png", "onboarding2.png", "onboarding3.png", "onboarding4.png"] - private let titleList: [String] = ["Prepare tools","Prepare tools", "Draw an angry man", "Ready"] - private let descriptionList: [String] = ["If you do not have a CPR mannequin,\nplease prepare a plastic bottle, pillow, etc.", "Put the plastic bottle inside the clothes\nyou don't wear and wrap it up.", "Draw an angry man on your clothes or pillow\nusing tape or pen.", "Please press the location marked in red!"] + private let practiceInfoList: [PracticeInfo] = { + var infoList: [PracticeInfo] = [] + + let imgList: [String] = ["onboarding1.png", "onboarding2.png", "onboarding3.png", "onboarding4.png"] + let titleList: [String] = ["pe_title_txt_1".localized(), "pe_title_txt_2".localized(), "pe_title_txt_3".localized(), "pe_title_txt_4".localized()] + let descriptionList: [String] = ["pe_des_txt_1".localized(), "pe_des_txt_2".localized(), "pe_des_txt_3".localized(), "pe_des_txt_4".localized()] + + for i in 0..= 80 { - descriptionLabel.text = "PASSED!" + descriptionLabel.text = "pass".localized() } else { - descriptionLabel.text = "FAILED..." + descriptionLabel.text = "fail".localized() } + print(scoreLabel.text) } + + private func arrangeScoreText(text: String) -> NSAttributedString { + var range: Int = 0 + let attributedString = NSMutableAttributedString(string: text) + if let rangeS = text.range(of: "/") { + range = text.distance(from: text.startIndex, to: rangeS.lowerBound) + } + + let realScoreAttributes: [NSAttributedString.Key: Any] = [ + .font : UIFont(weight: .bold, size: 72) ?? UIFont(), + .foregroundColor : UIColor.mainWhite + ] + + let defaultScoreAttributes: [NSAttributedString.Key: Any] = [ + .font : UIFont(weight: .bold, size: 32) ?? UIFont(), + .foregroundColor : UIColor.mainLightRed + ] + + attributedString.addAttributes(realScoreAttributes, range: NSRange(location: 0, length: range)) + attributedString.addAttributes(defaultScoreAttributes, range: NSRange(location: range, length: attributedString.length - range)) + + return attributedString + } + } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/CustomNoticeView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/CustomNoticeView.swift index 928975e..6e129ff 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/CustomNoticeView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/CustomNoticeView.swift @@ -5,15 +5,76 @@ // Created by 황정현 on 2023/03/10. // +import Combine import UIKit protocol CustomNoticeViewDelegate: AnyObject { func dismissQuizViewController() } -enum NoticeUsage { - case pf +enum NoticeUsage: Equatable { + case lecturePass + case quizFail(score: String) + case quizPass case certificate + case dispatchComplete + + var imageName: String { + switch self { + case .quizFail: + return "heart_fail" + case .lecturePass, .quizPass, .certificate, .dispatchComplete: + return "heart_success" + } + } + + var titleText: String { + switch self { + case .lecturePass: + return "lecture_pass_title_txt".localized() + case .quizFail(let score): + return String(format: "quiz_fail_title_txt_%@".localized(), score) + case .quizPass: + return "quiz_pass_title_txt".localized() + case .certificate: + return "certificate_title_txt".localized() + case .dispatchComplete: + return "dispatchComplete_title_txt".localized() + } + } + + var descriptionText: String { + switch self { + case .lecturePass: + return "lecture_pass_des_txt".localized() + case .quizFail: + return "quiz_fail_des_txt".localized() + case .quizPass: + return "quiz_pass_des_txt".localized() + case .certificate: + return "certificate_des_txt".localized() + case .dispatchComplete: + return "dispatchComplete_des_txt".localized() + } + } + + var confirmButtonTopAnchorMargin: CGFloat { + switch self { + case .lecturePass, .quizFail, .quizPass, .certificate: + return 24 + case .dispatchComplete: + return 0 + } + } + + var isNeedDelegate: Bool { + switch self { + case .quizFail, .quizPass: + return true + case .lecturePass, .certificate, .dispatchComplete: + return false + } + } } class CustomNoticeView: UIView { @@ -41,47 +102,64 @@ class CustomNoticeView: UIView { return view }() - private let thumbnailImageView: UIImageView = { + private lazy var thumbnailImageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFit return view }() - private let titleLabel: UILabel = { + private lazy var titleLabel: UILabel = { let label = UILabel() - label.font = UIFont(weight: .bold, size: 18) + label.font = UIFont(weight: .bold, size: 24) label.textAlignment = .center - label.textColor = .mainBlack + label.textColor = .black return label }() - private let subTitleLabel: UILabel = { + private lazy var descriptionLabel: UILabel = { let label = UILabel() label.font = UIFont(weight: .regular, size: 14) label.textAlignment = .center - label.textColor = .mainBlack + label.numberOfLines = 3 + label.textColor = UIColor(rgb: 0x525252) + return label }() let confirmButton: UIButton = { let button = UIButton() - button.layer.cornerRadius = 22 + button.layer.cornerRadius = 24 button.backgroundColor = .mainRed - button.titleLabel?.font = UIFont(weight: .bold, size: 17) + button.titleLabel?.font = UIFont(weight: .bold, size: 14) button.setTitleColor(.mainWhite, for: .normal) - button.setTitle("CONFIRM", for: .normal) + button.setTitle("got_it".localized(), for: .normal) return button }() + + private lazy var reportLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textColor = .mainBlack + label.textAlignment = .center + label.text = "report_title_txt".localized() + label.isUserInteractionEnabled = true + return label + }() + private let appearAnimDuration: CGFloat = 0.4 - private var noticeType = NoticeUsage.pf + private var noticeType: NoticeUsage? + private var dispatchId: Int? + private var cancellables = Set() + init(noticeAs: NoticeUsage) { super.init(frame: CGRect.zero) - + self.dispatchId = nil + noticeType = noticeAs + setUpComponent(noticeUsage: noticeAs) setUpConstraints() setUpStyle() setUpComponent() - noticeType = noticeAs } required init?(coder: NSCoder) { @@ -97,14 +175,14 @@ class CustomNoticeView: UIView { NSLayoutConstraint.activate([ noticeView.centerXAnchor.constraint(equalTo: self.centerXAnchor), noticeView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - noticeView.widthAnchor.constraint(equalToConstant: 313), - noticeView.heightAnchor.constraint(equalToConstant: 308) + noticeView.widthAnchor.constraint(equalToConstant: 314), + noticeView.heightAnchor.constraint(equalToConstant: 300) ]) [ thumbnailImageView, titleLabel, - subTitleLabel, + descriptionLabel, confirmButton ].forEach({ noticeView.addSubview($0) @@ -112,33 +190,37 @@ class CustomNoticeView: UIView { }) NSLayoutConstraint.activate([ - thumbnailImageView.topAnchor.constraint(equalTo: noticeView.topAnchor, constant: 40), - thumbnailImageView.leadingAnchor.constraint(equalTo: noticeView.leadingAnchor), - thumbnailImageView.trailingAnchor.constraint(equalTo: noticeView.trailingAnchor), - thumbnailImageView.heightAnchor.constraint(equalToConstant: 95) + thumbnailImageView.topAnchor.constraint(equalTo: noticeView.topAnchor, constant: 36), + thumbnailImageView.centerXAnchor.constraint(equalTo: noticeView.centerXAnchor), + thumbnailImageView.widthAnchor.constraint(equalToConstant: 54.78), + thumbnailImageView.heightAnchor.constraint(equalToConstant: 50) ]) NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: thumbnailImageView.bottomAnchor, constant: make.space24), + titleLabel.topAnchor.constraint(equalTo: thumbnailImageView.bottomAnchor, constant: make.space16), titleLabel.centerXAnchor.constraint(equalTo: noticeView.centerXAnchor), titleLabel.widthAnchor.constraint(equalTo: noticeView.widthAnchor), - titleLabel.heightAnchor.constraint(equalToConstant: 24) + titleLabel.heightAnchor.constraint(equalToConstant: 34) ]) NSLayoutConstraint.activate([ - subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: make.space2), - subTitleLabel.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), - subTitleLabel.widthAnchor.constraint(equalToConstant: 264), - subTitleLabel.heightAnchor.constraint(equalToConstant: 18) + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: make.space12), + descriptionLabel.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), + descriptionLabel.widthAnchor.constraint(equalToConstant: 264), + descriptionLabel.heightAnchor.constraint(equalToConstant: 64) ]) + guard let noticeType = noticeType else { return } NSLayoutConstraint.activate([ - confirmButton.bottomAnchor.constraint(equalTo: noticeView.bottomAnchor, constant: -26), + confirmButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: noticeType.confirmButtonTopAnchorMargin), confirmButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), - confirmButton.widthAnchor.constraint(equalToConstant: 206), - confirmButton.heightAnchor.constraint(equalToConstant: 44) + confirmButton.widthAnchor.constraint(equalToConstant: 230), + confirmButton.heightAnchor.constraint(equalToConstant: 48) ]) + if noticeType == .dispatchComplete { + makeReportLabel() + } } private func setUpStyle() { @@ -150,41 +232,9 @@ class CustomNoticeView: UIView { confirmButton.addTarget(self, action: #selector(didConfirmButtonTapped), for: .touchUpInside) } - func setCertificateNotice() { - setTitle(title: "Congratulation!") - guard let image = UIImage(named: "certificate_big.png") else { return } - setImage(uiImage: image) - setSubTitle(subTitle: "You have got CPR Angel Certificate!") - } - - func setPFResultNotice(isPass: Bool, quizResultString: String = ""){ - if isPass { - guard let image = UIImage(named: "success_heart.png") else { return } - setImage(uiImage: image) - setTitle(title: "Congratulation!") - setSubTitle(subTitle: "You are perfect!") - } else { - guard let image = UIImage(named: "fail_heart.png") else { return } - setImage(uiImage: image) - setTitle(title: "Failed \(quizResultString)") - setSubTitle(subTitle: "Try Again") - } - - } - private func setImage(uiImage: UIImage) { - thumbnailImageView.image = uiImage - } - - private func setTitle(title: String) { - titleLabel.text = title - } - - private func setSubTitle(subTitle: String) { - subTitleLabel.text = subTitle - } - @objc func didConfirmButtonTapped() { - if noticeType == .pf { + guard let noticeType = noticeType else { return } + if noticeType.isNeedDelegate == true { delegate?.dismissQuizViewController() } else { noticeDisappear() @@ -208,4 +258,66 @@ class CustomNoticeView: UIView { self?.removeFromSuperview() }) } + + func noticeHide() { + UIView.animate(withDuration: appearAnimDuration/2, delay: 0, animations: { + self.alpha = 0.0 + }) + } + + func makeReportLabel() { + noticeView.addSubview(reportLabel) + reportLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + reportLabel.topAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: Constraints.shared.space8), + reportLabel.centerXAnchor.constraint(equalTo: noticeView.centerXAnchor), + reportLabel.widthAnchor.constraint(equalToConstant: 264), + reportLabel.heightAnchor.constraint(equalToConstant: 20), + ]) + } + + // https://stackoverflow.com/questions/34649173/how-can-i-call-presentviewcontroller-in-uiview-class + func getCurrentViewController() -> UIViewController? { + if let rootController = UIApplication.shared.keyWindow?.rootViewController { + var currentController: UIViewController! = rootController + while( currentController.presentedViewController != nil ) { + currentController = currentController.presentedViewController + } + return currentController + } + return nil + + } + + func setUpComponent(noticeUsage: NoticeUsage) { + thumbnailImageView.image = UIImage(named: noticeUsage.imageName) + titleLabel.text = noticeUsage.titleText + descriptionLabel.text = noticeUsage.descriptionText + } + + func setUpQuizResult(isPassed: Bool, score: String) { + if isPassed { + setUpComponent(noticeUsage: .quizPass) + } else { + setUpComponent(noticeUsage: .quizFail(score: score)) + } + } + + func setUpDispatchComponent(dispatchId: Int) { + self.dispatchId = dispatchId + } + + func setUpAction(callVC: CallMainViewController, viewModel: CallViewModel) { + let tapGesture = UITapGestureRecognizer() + reportLabel.addGestureRecognizer(tapGesture) + tapGesture.tapPublisher.sink { [weak self] _ in + guard let self = self else { return } + guard let dispatchId = self.dispatchId else { return } + let vc = ReportViewController(dispatchId: dispatchId, viewModel: viewModel) + vc.delegate = callVC.self + vc.modalPresentationStyle = .fullScreen + guard let currentVC = getCurrentViewController() else { return } + currentVC.present(vc, animated: true) + }.store(in: &cancellables) + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/EducationQuizViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/EducationQuizViewController.swift index c75621e..a3c1933 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/EducationQuizViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/EducationQuizViewController.swift @@ -10,21 +10,22 @@ import Combine final class EducationQuizViewController: UIViewController { - private lazy var questionView = QuizQuestionView(questionNumber: 1, question: "When you find someone who has fallen, you have to compress his chest instantly.") + private var isPassed: Bool = false + private lazy var questionView = QuizQuestionView(questionNumber: 1, question: "") private lazy var oxChoiceView: OXQuizChoiceView = { - let view = OXQuizChoiceView(viewModel: viewModel) + let view = OXQuizChoiceView(viewModel: quizViewModel) view.alpha = 0 return view }() private lazy var multiChoiceView: MultiQuizChoiceView = { - let view = MultiQuizChoiceView(viewModel: viewModel) + let view = MultiQuizChoiceView(viewModel: quizViewModel) view.alpha = 0 return view }() - private lazy var noticeView = CustomNoticeView(noticeAs: .pf) + private lazy var noticeView = CustomNoticeView(noticeAs: .quizPass) private lazy var answerLabel: UILabel = { let label = UILabel() @@ -52,26 +53,33 @@ final class EducationQuizViewController: UIViewController { button.backgroundColor = .mainLightRed button.setTitleColor(.mainBlack, for: .normal) button.titleLabel?.font = UIFont(weight: .bold, size: 20) - button.setTitle("Confirm", for: .normal) + button.setTitle("confirm".localized(), for: .normal) return button }() - private let viewModel = QuizViewModel() + private var eduViewModel: EducationViewModel? + private let quizViewModel = QuizViewModel() private var cancellables = Set() weak var delegate: EducationMainViewControllerDelegate? + init(eduViewModel: EducationViewModel) { + super.init(nibName: nil, bundle: nil) + self.eduViewModel = eduViewModel + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func viewDidLoad() { super.viewDidLoad() setUpConstraints() setUpStyle() + setUpAction() setUpDelegate() - Task { - try await viewModel.receiveQuizList() - updateQuiz(quiz: viewModel.currentQuiz()) - } - bind(to: viewModel) + + bind(to: quizViewModel) } private func setUpConstraints() { @@ -108,7 +116,7 @@ final class EducationQuizViewController: UIViewController { oxChoiceView.topAnchor.constraint(equalTo: questionView.bottomAnchor, constant: 78), oxChoiceView.heightAnchor.constraint(equalToConstant: 80), multiChoiceView.topAnchor.constraint(equalTo: questionView.bottomAnchor, constant: 36), - multiChoiceView.heightAnchor.constraint(equalToConstant: 280) + multiChoiceView.heightAnchor.constraint(equalToConstant: 286) ]) NSLayoutConstraint.activate([ @@ -143,19 +151,32 @@ final class EducationQuizViewController: UIViewController { private func setUpStyle() { view.backgroundColor = .white - navigationController?.navigationBar.topItem?.title = "Quiz" - let closeItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(closeButtonTapped)) + navigationController?.navigationBar.topItem?.title = "course_quiz".localized() + let closeItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: nil) navigationItem.leftBarButtonItem = closeItem } + private func setUpAction() { + navigationItem.leftBarButtonItem?.tapPublisher.sink { [weak self] in + self?.closeQuiz() + }.store(in: &cancellables) + } + private func setUpDelegate() { noticeView.delegate = self } } -// MARK: ViewModel Binding extension EducationQuizViewController { private func bind(to viewModel: QuizViewModel) { + + viewModel.$quiz + .receive(on: DispatchQueue.main) + .sink { [weak self] quiz in + guard let quiz = quiz else { return } + self?.updateQuiz(quiz: quiz) + }.store(in: &cancellables) + viewModel.selectedAnswerIndex.sink { index in if (index != -1) { viewModel.isSelected() @@ -169,18 +190,18 @@ extension EducationQuizViewController { } private func nextQuiz() { - let output = viewModel.transform() + let output = quizViewModel.transform() output.isCorrect?.sink { [weak self] isCorrect in - guard let currentQuiz = self?.viewModel.currentQuiz() else { return } + guard let currentQuiz = self?.quizViewModel.quiz else { return } self?.answerLabel.isHidden = false self?.answerDescriptionLabel.isHidden = false self?.answerLabel.text = isCorrect ? "Correct!" : "Wrong!" self?.answerDescriptionLabel.text = currentQuiz.answerDescription self?.submitButton.setTitle("Next", for: .normal) - guard let answerIndex = self?.viewModel.currentQuiz().answerIndex, let quizType = self?.viewModel.currentQuiz().questionType else { + guard let answerIndex = self?.quizViewModel.quiz?.answerIndex, let quizType = self?.quizViewModel.quiz?.questionType else { return } switch quizType { @@ -194,30 +215,22 @@ extension EducationQuizViewController { }.store(in: &cancellables) - output.quiz?.sink { quiz in - self.updateQuiz(quiz: quiz) - - switch quiz.questionType { - case .ox: - self.oxChoiceView.interactionEnabled(to: true) - case .multi: - self.multiChoiceView.interactionEnabled(to: true) - } - }.store(in: &cancellables) - output.isQuizEnd.sink { [weak self] isQuizEnd in - guard let isQuizAllCorrect = self?.viewModel.isQuizAllCorrect() else { return } - guard let quizResultString = self?.viewModel.quizResultString() else { return } + guard let isQuizAllCorrect = self?.quizViewModel.isQuizAllCorrect() else { return } + guard let quizResultString = self?.quizViewModel.quizResultString() else { return } + + self?.isPassed = isQuizAllCorrect if isQuizEnd { + self?.noticeView.setUpQuizResult(isPassed: isQuizAllCorrect, score: quizResultString) if isQuizAllCorrect { Task { - try await self?.viewModel.saveQuizResult() - self?.noticeView.setPFResultNotice(isPass: true) - self?.noticeView.noticeAppear() + guard let isSucceed = try await self?.eduViewModel?.saveQuizResult() else { return } + if isSucceed { + self?.noticeView.noticeAppear() + } } } else { - self?.noticeView.setPFResultNotice(isPass: false, quizResultString: quizResultString) self?.noticeView.noticeAppear() } } @@ -225,16 +238,18 @@ extension EducationQuizViewController { } private func updateQuiz(quiz: Quiz) { - viewModel.updateSelectedAnswerIndex(index: -1) + quizViewModel.updateSelectedAnswerIndex(index: -1) questionView.setUpText(questionNumber: quiz.questionNumber, question: quiz.question) switch quiz.questionType { case .ox: updateChoiceView(current: multiChoiceView, as: oxChoiceView) oxChoiceView.setUpText() + oxChoiceView.interactionEnabled(to: true) case .multi: updateChoiceView(current: oxChoiceView, as: multiChoiceView) multiChoiceView.setUpText(quiz.answerList) + multiChoiceView.interactionEnabled(to: true) } oxChoiceView.resetChoiceButtonConstraint() @@ -242,7 +257,7 @@ extension EducationQuizViewController { [answerLabel, answerDescriptionLabel].forEach{ $0.isHidden = true } answerDescriptionLabel.text = quiz.answerDescription - submitButton.setTitle("Confirm", for: .normal) + submitButton.setTitle("confirm".localized(), for: .normal) } private func updateChoiceView(current: QuizChoiceView, as will: QuizChoiceView) { @@ -251,18 +266,15 @@ extension EducationQuizViewController { will.alpha = 1.0 will.isUserInteractionEnabled = true } -} - -// MARK: Objc Function -extension EducationQuizViewController { - @objc private func closeButtonTapped() { - let alert = UIAlertController(title: "Quiz Exit", message: "All progress will be lost", preferredStyle: .alert) + + private func closeQuiz() { + let alert = UIAlertController(title: "quiz_exit".localized(), message: "quiz_exit_warn_txt".localized(), preferredStyle: .alert) - let confirm = UIAlertAction(title: "Confirm", style: .destructive, handler: { _ in + let confirm = UIAlertAction(title: "confirm".localized(), style: .destructive, handler: { _ in self.dismiss(animated: true) }) - let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + let cancel = UIAlertAction(title: "cancel".localized(), style: .cancel, handler: nil) [confirm, cancel].forEach { alert.addAction($0) } @@ -274,6 +286,6 @@ extension EducationQuizViewController { // MARK: Delegate extension EducationQuizViewController: CustomNoticeViewDelegate { func dismissQuizViewController() { - delegate?.updateUserEducationStatus() + delegate?.updateUserEducationStatus(isPassed: isPassed) } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/MultiQuizChoiceView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/MultiQuizChoiceView.swift index 66f21f7..898345f 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/MultiQuizChoiceView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/MultiQuizChoiceView.swift @@ -30,6 +30,8 @@ final class MultiQuizChoiceView: QuizChoiceView { } override func setUpConstraints() { + let make = Constraints.shared + let stackView = UIStackView() stackView.axis = NSLayoutConstraint.Axis.vertical stackView.distribution = UIStackView.Distribution.equalSpacing @@ -39,30 +41,19 @@ final class MultiQuizChoiceView: QuizChoiceView { self.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false - - choices.forEach({ - stackView.addSubview($0) - $0.translatesAutoresizingMaskIntoConstraints = false - - $0.widthAnchor.constraint(equalToConstant: 334).isActive = true - $0.heightAnchor.constraint(equalToConstant: 52).isActive = true - $0.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true - }) - NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: self.topAnchor), stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), - stackView.widthAnchor.constraint(equalToConstant: 260) - ]) - - NSLayoutConstraint.activate([ - choices[0].topAnchor.constraint(equalTo: stackView.topAnchor), - choices[1].topAnchor.constraint(equalTo: choices[0].bottomAnchor, constant: 26), - choices[2].topAnchor.constraint(equalTo: choices[1].bottomAnchor, constant: 26), - choices[3].topAnchor.constraint(equalTo: choices[2].bottomAnchor, constant: 26) + stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: make.space12), + stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -make.space12) ]) + choices.forEach({ + stackView.addArrangedSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.heightAnchor.constraint(equalToConstant: 52).isActive = true + $0.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true + }) answerCenterPosition = choices[1].center } @@ -73,9 +64,7 @@ final class MultiQuizChoiceView: QuizChoiceView { $0.layer.borderColor = UIColor.mainRed.cgColor $0.layer.cornerRadius = 20 $0.titleLabel?.font = UIFont(weight: .regular, size: 26) - $0.titleLabel?.minimumScaleFactor = 0.15 $0.titleLabel?.numberOfLines = 1 - $0.titleLabel?.adjustsFontSizeToFitWidth = true $0.titleLabel?.lineBreakMode = NSLineBreakMode.byClipping $0.setTitleColor(.mainBlack, for: .normal) }) diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizChoiceView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizChoiceView.swift index 370eb58..c008995 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizChoiceView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizChoiceView.swift @@ -48,7 +48,7 @@ public class QuizChoiceView: UIView { if index == -1 { self?.resetButtonStatus() } else { - guard quizType == viewModel.currentQuizType() else { return } + guard quizType == viewModel.quiz?.questionType else { return } self?.choices[index].changeButtonStyle(isSelected: true) let otherChoices = self?.choices.filter { $0 != self?.choices[index] } otherChoices?.forEach({ @@ -70,7 +70,14 @@ public class QuizChoiceView: UIView { choices[1].setTitle("X", for: .normal) } else { for (index, choice) in choices.enumerated() { - choice.setTitle(answers?[index], for: .normal) + guard let answer = answers?[index] else { return } + choice.setTitle(answer, for: .normal) + + if answer.count > 24 { + choice.titleLabel?.font = UIFont(weight: .regular, size: 20) + } else { + choice.titleLabel?.font = UIFont(weight: .regular, size: 26) + } } } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizViewModel.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizViewModel.swift index ec957d7..63ee687 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizViewModel.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Education/Quiz/QuizViewModel.swift @@ -8,7 +8,9 @@ import Foundation import Combine -class QuizViewModel: OutputOnlyViewModelType { +final class QuizViewModel: OutputOnlyViewModelType { + @Published private(set)var quiz: Quiz? + private var eduManager: EducationManager private var quizList: [Quiz] = [] private var currentQuizIndex: Int = 0 @@ -18,10 +20,10 @@ class QuizViewModel: OutputOnlyViewModelType { init() { eduManager = EducationManager(service: APIManager()) + receiveQuizList() } struct Output { - let quiz: CurrentValueSubject? let isCorrect: CurrentValueSubject? let isQuizEnd: CurrentValueSubject } @@ -38,12 +40,6 @@ class QuizViewModel: OutputOnlyViewModelType { return quizList[0] } - func currentQuiz() -> Quiz { - return quizList[currentQuizIndex] - } - func currentQuizType() -> QuizType { - return quizList[currentQuizIndex].questionType - } func updateSelectedAnswerIndex(index: Int) { selectedAnswerIndex.send(index) } @@ -59,26 +55,28 @@ class QuizViewModel: OutputOnlyViewModelType { func transform() -> Output { if selectedAnswerIndex.value == -1 { - return Output(quiz: nil, isCorrect: nil, isQuizEnd: CurrentValueSubject(false)) + return Output(isCorrect: nil, isQuizEnd: CurrentValueSubject(false)) } var output: Output if didSelectAnswer { let index = selectedAnswerIndex.value - let isCorrect = currentQuiz().answerIndex == index + let isCorrect = quiz?.answerIndex == index if isCorrect { correctQuizNum += 1 } - output = Output(quiz: nil, isCorrect: CurrentValueSubject(isCorrect), isQuizEnd: CurrentValueSubject(false)) + output = Output(isCorrect: CurrentValueSubject(isCorrect), isQuizEnd: CurrentValueSubject(false)) didSelectAnswer.toggle() } else { currentQuizIndex += 1 + print(currentQuizIndex, "/", quizList.count-1) if quizList.count == currentQuizIndex { - output = Output(quiz: nil, isCorrect: nil, isQuizEnd: CurrentValueSubject(true)) + output = Output(isCorrect: nil, isQuizEnd: CurrentValueSubject(true)) } else { - output = Output(quiz: CurrentValueSubject(quizList[currentQuizIndex]), isCorrect: nil, isQuizEnd: CurrentValueSubject(false)) + quiz = quizList[currentQuizIndex] + output = Output(isCorrect: nil, isQuizEnd: CurrentValueSubject(false)) selectedAnswerIndex.send(-1) didSelectAnswer.toggle() } @@ -86,28 +84,18 @@ class QuizViewModel: OutputOnlyViewModelType { return output } - func receiveQuizList() async throws { - let result = Task { () -> [QuizInfo]? in + func receiveQuizList() { + Task { let eduResult = try await eduManager.getQuizList() - return eduResult.data - } - - do { - let data = try await result.value - data?.forEach({ item in - - guard let answerIndex = item.answer_list.map({ $0.id }).firstIndex(of: item.answer) else { return } - let answerList = item.answer_list.map { $0.content } - let quiz = Quiz(questionType: item.type == 0 ? .ox : .multi, questionNumber: item.index, question: item.question, answerIndex: answerIndex, answerList: answerList, answerDescription: item.reason) - quizList.append(quiz) + eduResult.data?.forEach({ item in + + guard let answerIndex = item.answer_list.map({ $0.id }).firstIndex(of: item.answer) else { return } + let answerList = item.answer_list.map { $0.content } + let quiz = Quiz(questionType: item.type == 0 ? .ox : .multi, questionNumber: item.index, question: item.question, answerIndex: answerIndex, answerList: answerList, answerDescription: item.reason) + quizList.append(quiz) }) - } catch(let error) { - print(error) + + quiz = quizList[currentQuizIndex] } } - - func saveQuizResult() async throws{ - (_, _) = try await eduManager.saveQuizResult(score: 100) - } - } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/AuthViewModel.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/AuthViewModel.swift similarity index 50% rename from CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/AuthViewModel.swift rename to CPR2U-iOS/CPR2U/CPR2U/Scene/Login/AuthViewModel.swift index 8296062..47255d5 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/AuthViewModel.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/AuthViewModel.swift @@ -14,49 +14,56 @@ enum LoginPhase { case Nickname } -protocol ViewModelType { - associatedtype Input - associatedtype Output - - func transform(loginPhase: LoginPhase, input: Input) -> Output -} - -final class AuthViewModel: ViewModelType { - +final class AuthViewModel: AuthViewModelType { private let authManager: AuthManager - private var phoneNumberString: String? - private var smsCode: String? - private var nickname: String? - - init() { - authManager = AuthManager(service: APIManager()) - } - func getPhoneNumber() -> String { - guard let str = phoneNumberString else { return "" } - return "+82 \(str)" - } + private var _phoneNumber: String? + private var _smsCode: String? + private var _nickname: String? + private var _addressId: Int? - func getSMSCode() -> String { - guard let str = smsCode else { return "" } - return str + var phoneNumber: String { + get { + guard let str = _phoneNumber else { return "" } + return str + } + set(value) { + _phoneNumber = value + } } - func getNickname() -> String { - guard let str = nickname else { return "" } - return str + var smsCode: String { + get { + guard let str = _smsCode else { return "" } + return str + } + set(value) { + _smsCode = value + } } - func setPhoneNumber(number: String) { - phoneNumberString = number + var nickname: String { + get { + guard let str = _nickname else { return "" } + return str + } + set(value) { + _nickname = value + } } - func setSMSCode(number: String) { - smsCode = number + var addressId: Int { + get { + guard let value = _addressId else { return -1 } + return value + } + set(value) { + _addressId = value + } } - func setNickname(name: String) { - nickname = name + init() { + authManager = AuthManager(service: APIManager()) } func autoLogin() async throws -> Bool { @@ -81,7 +88,7 @@ final class AuthViewModel: ViewModelType { let taskResult = Task { () -> String? in var result: SMSCodeResult? do { - (_, result) = try await authManager.phoneNumberVerify(phoneNumber: phoneNumber) + (_, result) = try await authManager.phoneNumberVerify(phoneNumber: "+82\(phoneNumber)") } catch (let error) { print(error) } @@ -96,10 +103,15 @@ final class AuthViewModel: ViewModelType { func userVerify() async throws -> Bool { let result = Task { () -> Bool in - guard let phoneNumber = phoneNumberString else { return false } - let authResult = try await authManager.signIn(phoneNumber: phoneNumber, deviceToken: DeviceTokenManager.deviceToken) - - return authResult.success + if phoneNumber == "" { + return false + } else { + let authResult = try await authManager.signIn(phoneNumber: phoneNumber, deviceToken: DeviceTokenManager.deviceToken) + guard let data = authResult.data else { return false } + UserDefaultsManager.accessToken = data.access_token + UserDefaultsManager.refreshToken = data.refresh_token + return authResult.success + } } return try await result.value } @@ -119,31 +131,60 @@ final class AuthViewModel: ViewModelType { func signUp() async throws -> Bool { let taskResult = Task { () -> Bool in - guard let phoneNumber = phoneNumberString, let nickname = nickname else { return false } - let authResult = try await authManager.signUp(nickname: nickname, phoneNumber: phoneNumber, deviceToken: DeviceTokenManager.deviceToken) + if phoneNumber == "" || nickname == "" || addressId == -1 { + return false + } else { + let authResult = try await authManager.signUp(nickname: nickname, phoneNumber: phoneNumber, addressId: addressId, deviceToken: DeviceTokenManager.deviceToken) + if authResult.success == true { + guard let data = authResult.data else { return false } + UserDefaultsManager.accessToken = data.access_token + UserDefaultsManager.refreshToken = data.refresh_token + print("USER TOKEN UPDATE") + } + return authResult.success + } + } + return try await taskResult.value + } + + func getAddressList() async throws -> [AddressListResult]? { + let taskResult = Task { () -> [AddressListResult]? in + let authResult = try await authManager.getAddressList() if authResult.success == true { - guard let data = authResult.data else { return false } - UserDefaultsManager.accessToken = data.access_token - UserDefaultsManager.refreshToken = data.refresh_token - print("USER TOKEN UPDATE") + return authResult.data + } else { + return nil } + } + return try await taskResult.value + } + + func logOut() async throws -> Bool { + let taskResult = Task { () -> Bool in + let authResult = try await authManager.logOut() return authResult.success } return try await taskResult.value } struct Input { - let verifier: AnyPublisher + let verifier: AnyPublisher } struct Output { - let buttonIsValid: AnyPublisher + let buttonIsValid: AnyPublisher? } func transform(loginPhase: LoginPhase, input: Input) -> Output { - let buttonStatePublisher = input.verifier.map { verifier in - verifier.count > 0 + + let buttonStatePublisher = input.verifier.map { text in + if let length = text?.count { + return length > 0 + } else { + return false + } }.eraseToAnyPublisher() + return Output(buttonIsValid: buttonStatePublisher) } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/AddressVerificationViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/AddressVerificationViewController.swift new file mode 100644 index 0000000..18e9bcf --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/AddressVerificationViewController.swift @@ -0,0 +1,274 @@ +// +// AddressVerificationViewController.swift +// CPR2U +// +// Created by 황정현 on 2023/05/24. +// + +import Combine +import UIKit + +final class AddressVerificationViewController: UIViewController { + + private let instructionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 24) + label.textColor = .mainBlack + label.text = "address_ins_txt".localized() + return label + }() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textColor = .mainBlack + label.text = "address_des_txt".localized() + return label + }() + + private lazy var mainAddressTextField: TextField = { + let textField = TextField() + textField.font = UIFont(weight: .regular, size: 16) + textField.textColor = UIColor(rgb: 0xC1C1C1) + textField.textAlignment = .left + textField.layer.borderWidth = 1 + textField.layer.cornerRadius = 6 + textField.layer.borderColor = UIColor.black.withAlphaComponent(0.1).cgColor + textField.tintColor = .clear + textField.text = "시/도" + return textField + }() + + private lazy var subAddressTextField: TextField = { + let textField = TextField() + textField.font = UIFont(weight: .regular, size: 16) + textField.textColor = UIColor(rgb: 0xC1C1C1) + textField.textAlignment = .left + textField.layer.borderWidth = 1 + textField.layer.cornerRadius = 6 + textField.layer.borderColor = UIColor.black.withAlphaComponent(0.1).cgColor + textField.tintColor = .clear + textField.text = "구/군" + return textField + }() + + private let continueButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont(weight: .bold, size: 16) + button.setTitle("continue".localized(), for: .normal) + button.setTitleColor(.mainWhite, for: .normal) + button.backgroundColor = .mainRed + button.layer.cornerRadius = 27.5 + return button + }() + + private var addressList: [AddressListResult] = [] + private var mainAddressIndex: Int? + private var addressId: Int? + + private let addressManager = AddressManager(service: APIManager()) + private var viewModel: AuthViewModel + private var cancellables = Set() + + init(viewModel: AuthViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setUpConstraints() + setUpStyle() + bind(viewModel: viewModel) + + Task { + setUpAddreessList() + } + } + + private func setUpConstraints() { + let make = Constraints.shared + let safeArea = view.safeAreaLayoutGuide + + [ + instructionLabel, + descriptionLabel, + mainAddressTextField, + subAddressTextField, + continueButton + ].forEach({ + view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + }) + + NSLayoutConstraint.activate([ + instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: make.space16), + instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), + instructionLabel.heightAnchor.constraint(equalToConstant: 32) + ]) + + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: make.space4), + descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: make.space16), + descriptionLabel.heightAnchor.constraint(equalToConstant: 28) + ]) + + NSLayoutConstraint.activate([ + mainAddressTextField.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 32), + mainAddressTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: make.space16), + mainAddressTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -make.space16), + mainAddressTextField.heightAnchor.constraint(equalToConstant: 50) + ]) + + NSLayoutConstraint.activate([ + subAddressTextField.topAnchor.constraint(equalTo: mainAddressTextField.bottomAnchor, constant: make.space8), + subAddressTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: make.space16), + subAddressTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -make.space16), + subAddressTextField.heightAnchor.constraint(equalToConstant: 50) + ]) + + NSLayoutConstraint.activate([ + continueButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + continueButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), + continueButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -make.space16), + continueButton.heightAnchor.constraint(equalToConstant: 55) + ]) + + } + + private func setUpStyle() { + view.backgroundColor = .white + } + + private func bind(viewModel: AuthViewModel) { + continueButton.tapPublisher.sink { [weak self] in + Task { + let signUpResult = try await self?.viewModel.signUp() + if signUpResult == true { + self?.dismiss(animated: true) + let vc = TabBarViewController() + guard let window = self?.view.window else { return } + await window.setRootViewController(vc, animated: true) + } else { + print("에러") + } + } + }.store(in: &cancellables) + } + + private func setUpAddreessList() { + Task { + guard let data = try await viewModel.getAddressList() else { return } + addressList = data + setUpPickerView() + } + } + + private func setUpPickerView() { + let mainPickerView = UIPickerView() + mainPickerView.layer.name = "mainPickerView" + let subPickerView = UIPickerView() + subPickerView.layer.name = "subPickerView" + + [mainPickerView, subPickerView].forEach({ + $0.delegate = self + $0.dataSource = self + }) + + [mainAddressTextField, subAddressTextField].forEach({ + let toolBar = UIToolbar() + toolBar.sizeToFit() + let button = UIBarButtonItem(title: "완료", style: .plain, target: self, action: #selector(self.didSelectButtonTapped)) + toolBar.setItems([button], animated: true) + toolBar.isUserInteractionEnabled = true + $0.inputAccessoryView = toolBar + }) + + mainAddressTextField.inputView = mainPickerView + subAddressTextField.inputView = subPickerView + } + + private func setUpAction() { + continueButton.tapPublisher.sink { + Task { [weak self] in + guard let self = self else { return } + let signUpResult = try await self.viewModel.signUp() + if signUpResult == true { + self.dismiss(animated: true) + let vc = TabBarViewController() + guard let window = self.view.window else { return } + await window.setRootViewController(vc, animated: true) + } else { + print("에러") + } + } + }.store(in: &cancellables) + } + + @objc func didSelectButtonTapped() { + mainAddressTextField.endEditing(true) + subAddressTextField.endEditing(true) + } +} + +extension AddressVerificationViewController: UIPickerViewDelegate, UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + if pickerView.layer.name == "mainPickerView" { + return 1 + } else if pickerView.layer.name == "subPickerView" { + return 1 + } + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + if pickerView.layer.name == "mainPickerView" { + return addressList.count + } else if pickerView.layer.name == "subPickerView" { + guard let index = mainAddressIndex else { return 0 } + return addressList[index].gugun_list.count + } + return 0 + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + if pickerView.layer.name == "mainPickerView" { + return addressList[row].sido + } else if pickerView.layer.name == "subPickerView" { + guard let index = mainAddressIndex else { return "" } + return addressList[index].gugun_list[row].gugun + } + return nil + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + if pickerView.layer.name == "mainPickerView" { + mainAddressTextField.text = addressList[row].sido + mainAddressTextField.textColor = .mainBlack + if addressList[row].sido == "세종특별자치시" { + viewModel.addressId = addressList[row].gugun_list[0].id + subAddressTextField.isHidden = true + print("ADDRESS ID IS \(viewModel.addressId)") + } else { + addressId = nil + subAddressTextField.text = "구/군" + subAddressTextField.textColor = UIColor(rgb: 0xC1C1C1) + subAddressTextField.isHidden = false + } + mainAddressIndex = row + } else if pickerView.layer.name == "subPickerView" { + guard let index = mainAddressIndex else { return } + subAddressTextField.text = addressList[index].gugun_list[row].gugun + subAddressTextField.textColor = .mainBlack + viewModel.addressId = addressList[index].gugun_list[row].id + print("ADDRESS ID IS \(viewModel.addressId)") + } + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/NicknameVertificationViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/NicknameVertificationViewController.swift index b255e0b..63b26b8 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/NicknameVertificationViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/NicknameVertificationViewController.swift @@ -21,9 +21,10 @@ enum NicknameStatus { var str: String switch self { case .specialCharacters: - str = "Nickname cannot contain special characters" + str = "nickname_special_character".localized() case .unavailable: - str = "\'\(name)' is Unavailable" + let localizedStr = String(format: "%@_nickname_unavailable".localized(), name) + str = localizedStr case .available: str = "" case .none: @@ -47,17 +48,58 @@ final class NicknameVerificationViewController: UIViewController { private let signManager = AuthManager(service: APIManager()) - var phoneNumberString: String? + // MARK: 기존 회원인 경우와 기존 회원이 아닌 경우를 나누어서 처리가 이루어져야함 + private let instructionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 24) + label.textColor = .mainBlack + label.text = "nickname_ins_txt".localized() + return label + }() - private let instructionLabel = UILabel() - private let descriptionLabel = UILabel() + // MARK: 기존 회원인 경우와 기존 회원이 아닌 경우를 나누어서 처리가 이루어져야함 + private let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textColor = .mainBlack + label.text = "nickname_des_txt".localized() + return label + }() - private let nicknameView = UIView() - private let nicknameTextField = UITextField() + private let nicknameView: UIView = { + let view = UIView() + view.layer.borderColor = UIColor(rgb:0xF2F2F2).cgColor + view.layer.borderWidth = 1 + view.layer.cornerRadius = 6 + return view + }() - private let irregularNoticeLabel = UILabel() + private let nicknameTextField: UITextField = { + let textField = UITextField() + textField.backgroundColor = .clear + textField.textColor = .mainBlack + textField.font = UIFont(weight: .regular, size: 16) + textField.placeholder = "nickname_phdr".localized() + return textField + }() - private let continueButton = UIButton() + private let irregularNoticeLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textAlignment = .left + label.textColor = .mainRed + return label + }() + + private let continueButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont(weight: .bold, size: 16) + button.setTitle("continue".localized(), for: .normal) + button.setTitleColor(.mainWhite, for: .normal) + button.backgroundColor = .mainRed + button.layer.cornerRadius = 27.5 + return button + }() private var continueButtonBottomConstraints = NSLayoutConstraint() @@ -73,7 +115,6 @@ final class NicknameVerificationViewController: UIViewController { init(viewModel: AuthViewModel) { self.viewModel = viewModel - self.phoneNumberString = viewModel.getPhoneNumber() super.init(nibName: nil, bundle: nil) } @@ -87,18 +128,13 @@ final class NicknameVerificationViewController: UIViewController { setUpConstraints() setUpStyle() - setUpText() setUpAction() setUpKeyboard() bind(viewModel: viewModel) } private func setUpConstraints() { - - let space4: CGFloat = 4 - let space8: CGFloat = 8 - let space16: CGFloat = 16 - + let make = Constraints.shared let safeArea = view.safeAreaLayoutGuide [ @@ -116,86 +152,79 @@ final class NicknameVerificationViewController: UIViewController { nicknameTextField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: space16), - instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: make.space16), + instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), instructionLabel.heightAnchor.constraint(equalToConstant: 32) ]) NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: space4), - descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: space16), + descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: make.space4), + descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: make.space16), descriptionLabel.heightAnchor.constraint(equalToConstant: 28) ]) NSLayoutConstraint.activate([ - nicknameView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: space8), - nicknameView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - nicknameView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + nicknameView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: make.space8), + nicknameView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + nicknameView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), nicknameView.heightAnchor.constraint(equalToConstant: 48) ]) NSLayoutConstraint.activate([ nicknameTextField.topAnchor.constraint(equalTo: nicknameView.topAnchor), - nicknameTextField.leadingAnchor.constraint(equalTo: nicknameView.leadingAnchor, constant: space16), + nicknameTextField.leadingAnchor.constraint(equalTo: nicknameView.leadingAnchor, constant: make.space16), nicknameTextField.trailingAnchor.constraint(equalTo: nicknameView.trailingAnchor), nicknameTextField.heightAnchor.constraint(equalTo: nicknameView.heightAnchor) ]) NSLayoutConstraint.activate([ - irregularNoticeLabel.topAnchor.constraint(equalTo: nicknameTextField.bottomAnchor, constant: space8), - irregularNoticeLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), + irregularNoticeLabel.topAnchor.constraint(equalTo: nicknameTextField.bottomAnchor, constant: make.space8), + irregularNoticeLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), irregularNoticeLabel.widthAnchor.constraint(equalToConstant: 300), irregularNoticeLabel.heightAnchor.constraint(equalToConstant: 16), ]) - continueButtonBottomConstraints = continueButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -space16) + continueButtonBottomConstraints = continueButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -make.space16) NSLayoutConstraint.activate([ - continueButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - continueButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + continueButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + continueButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), continueButtonBottomConstraints, continueButton.heightAnchor.constraint(equalToConstant: 55) ]) } private func setUpStyle() { - view.backgroundColor = .white - - instructionLabel.font = UIFont(weight: .bold, size: 24) - instructionLabel.textColor = .mainBlack - descriptionLabel.font = UIFont(weight: .regular, size: 14) - descriptionLabel.textColor = .mainBlack - - nicknameView.layer.borderColor = UIColor(rgb:0xF2F2F2).cgColor - nicknameView.layer.borderWidth = 1 - nicknameView.layer.cornerRadius = 6 - - nicknameTextField.backgroundColor = .clear - nicknameTextField.textColor = .mainBlack - nicknameTextField.font = UIFont(weight: .regular, size: 16) - - irregularNoticeLabel.font = UIFont(weight: .regular, size: 14) - irregularNoticeLabel.textAlignment = .left - irregularNoticeLabel.textColor = .mainRed - - continueButton.titleLabel?.font = UIFont(weight: .bold, size: 16) - continueButton.setTitleColor(.mainWhite, for: .normal) - continueButton.backgroundColor = .mainRed - continueButton.layer.cornerRadius = 27.5 - } - - private func setUpText() { - instructionLabel.text = "Enter your Nickname" - descriptionLabel.text = "People can recognize you by your nickname" - continueButton.setTitle("CONTINUE", for: .normal) - - nicknameTextField.placeholder = "Nickname*" } private func setUpAction() { - nicknameTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) + nicknameTextField.textPublisher.sink { [weak self] text in + guard let text else { return } + + if text.count > 20 { + self?.nicknameTextField.text?.removeLast() + } + + let strArr = Array(text) + let pattern = "^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9]$" + + if strArr.count > 0 { + if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) { + for index in 0.. 20 { - textField.text?.removeLast() - } - - let strArr = Array(str) - let pattern = "^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9]$" - - if strArr.count > 0 { - if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) { - for index in 0.. 0 else { return } - if (self?.nicknameStatus != .specialCharacters) { - let nicknameStatus = try await self?.viewModel.nicknameVerify(userInput: userInput) + guard let self = self, userInput.count > 0 else { return } + if (self.nicknameStatus != .specialCharacters) { + let nicknameStatus = try await self.viewModel.nicknameVerify(userInput: userInput) if nicknameStatus == .available { - self?.viewModel.setNickname(name: userInput) - let signUpResult = try await self?.viewModel.signUp() - if signUpResult == true { - self?.dismiss(animated: true) - let vc = TabBarViewController() - guard let window = self?.view.window else { return } - await window.setRootViewController(vc, animated: true) - } else { - print("에러") - } + viewModel.nickname = userInput + self.navigationController?.pushViewController(AddressVerificationViewController(viewModel: viewModel), animated: true) } else { - guard let label = self?.irregularNoticeLabel else { return } - nicknameStatus?.changeNoticeLabel(noticeLabel: label, nickname: userInput) + nicknameStatus.changeNoticeLabel(noticeLabel: self.irregularNoticeLabel, nickname: userInput) } } } @@ -269,7 +263,7 @@ final class NicknameVerificationViewController: UIViewController { } @objc private func keyboardWillHide(_ notification: Notification) { - continueButtonBottomConstraints.constant = -16 + continueButtonBottomConstraints.constant = -Constraints.shared.space16 view.layoutIfNeeded() } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/PhoneNumberVertificationViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/PhoneNumberVertificationViewController.swift index 9aaf44d..f657856 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/PhoneNumberVertificationViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/PhoneNumberVertificationViewController.swift @@ -10,17 +10,63 @@ import CombineCocoa import UIKit final class PhoneNumberVerificationViewController: UIViewController { - - private let instructionLabel = UILabel() - private let descriptionLabel = UILabel() + private let instructionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .bold, size: 24) + label.textColor = .mainBlack + label.text = "pn_ins_txt".localized() + return label + }() + private let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 14) + label.textColor = .mainBlack + label.text = "pn_des_txt".localized() + return label + }() + + private let phoneNumberView: UIView = { + let view = UIView() + view.layer.borderColor = UIColor(rgb:0xF2F2F2).cgColor + view.layer.borderWidth = 1 + view.layer.cornerRadius = 6 + return view + }() + + private let phoneNumberNationView: UIView = { + let view = UIView() + view.clipsToBounds = true + view.layer.cornerRadius = 6 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + view.backgroundColor = UIColor(rgb:0xF2F2F2) + return view + }() + + private let phoneNumberNationLabel: UILabel = { + let label = UILabel() + label.font = UIFont(weight: .regular, size: 15) + label.textAlignment = .center + label.textColor = .mainBlack + label.text = "nation_code".localized() + return label + }() - private let phoneNumberView = UIView() - private let phoneNumberNationView = UIView() - private let phoneNumberNationLabel = UILabel() - private let phoneNumberTextField = UITextField() + private let phoneNumberTextField: UITextField = { + let textField = UITextField() + textField.backgroundColor = .clear + textField.textColor = .mainBlack + textField.font = UIFont(weight: .regular, size: 16) + textField.placeholder = "pn_phdr".localized() + return textField + }() private let sendButton: UIButton = { let button = UIButton() + button.titleLabel?.font = UIFont(weight: .bold, size: 16) + button.setTitle("send".localized(), for: .normal) + button.setTitleColor(.mainBlack, for: .normal) + button.backgroundColor = .mainLightGray + button.layer.cornerRadius = 27.5 button.isEnabled = false return button }() @@ -44,17 +90,12 @@ final class PhoneNumberVerificationViewController: UIViewController { setUpConstraints() setUpStyle() - setUpText() setUpKeyboard() bind(to: viewModel) } private func setUpConstraints() { - - let space4: CGFloat = 4 - let space8: CGFloat = 8 - let space16: CGFloat = 16 - + let make = Constraints.shared let safeArea = view.safeAreaLayoutGuide [ @@ -76,23 +117,23 @@ final class PhoneNumberVerificationViewController: UIViewController { }) NSLayoutConstraint.activate([ - instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: space16), - instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: make.space16), + instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), instructionLabel.heightAnchor.constraint(equalToConstant: 32) ]) NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: space4), - descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: space16), + descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: make.space4), + descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: make.space16), descriptionLabel.heightAnchor.constraint(equalToConstant: 28) ]) NSLayoutConstraint.activate([ - phoneNumberView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: space8), - phoneNumberView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - phoneNumberView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + phoneNumberView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: make.space8), + phoneNumberView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + phoneNumberView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), phoneNumberView.heightAnchor.constraint(equalToConstant: 48) ]) @@ -114,62 +155,24 @@ final class PhoneNumberVerificationViewController: UIViewController { NSLayoutConstraint.activate([ phoneNumberTextField.topAnchor.constraint(equalTo: phoneNumberView.topAnchor), - phoneNumberTextField.leadingAnchor.constraint(equalTo: phoneNumberNationView.trailingAnchor, constant: space16), + phoneNumberTextField.leadingAnchor.constraint(equalTo: phoneNumberNationView.trailingAnchor, constant: make.space16), phoneNumberTextField.trailingAnchor.constraint(equalTo: phoneNumberView.trailingAnchor), phoneNumberTextField.heightAnchor.constraint(equalTo: phoneNumberView.heightAnchor) ]) - sendButtonBottomConstraints = sendButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -space16) + sendButtonBottomConstraints = sendButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -make.space16) NSLayoutConstraint.activate([ - sendButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - sendButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + sendButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + sendButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), sendButtonBottomConstraints, sendButton.heightAnchor.constraint(equalToConstant: 55) ]) } private func setUpStyle() { - view.backgroundColor = .white - - instructionLabel.font = UIFont(weight: .bold, size: 24) - instructionLabel.textColor = .mainBlack - descriptionLabel.font = UIFont(weight: .regular, size: 14) - descriptionLabel.textColor = .mainBlack - - phoneNumberNationView.clipsToBounds = true - phoneNumberNationView.layer.cornerRadius = 6 - phoneNumberNationView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - phoneNumberNationView.backgroundColor = UIColor(rgb:0xF2F2F2) - - phoneNumberNationLabel.font = UIFont(weight: .regular, size: 15) - phoneNumberNationLabel.textAlignment = .center - phoneNumberNationLabel.textColor = .mainBlack - - phoneNumberView.layer.borderColor = UIColor(rgb:0xF2F2F2).cgColor - phoneNumberView.layer.borderWidth = 1 - phoneNumberView.layer.cornerRadius = 6 - - phoneNumberTextField.backgroundColor = .clear - phoneNumberTextField.textColor = .mainBlack - phoneNumberTextField.font = UIFont(weight: .regular, size: 16) - - sendButton.titleLabel?.font = UIFont(weight: .bold, size: 16) - sendButton.setTitleColor(.mainBlack, for: .normal) - sendButton.backgroundColor = .mainLightGray - sendButton.layer.cornerRadius = 27.5 } - private func setUpText() { - instructionLabel.text = "Enter your number" - descriptionLabel.text = "We will send a code to verify your mobile number" - - phoneNumberNationLabel.text = "+ 82" - - phoneNumberTextField.placeholder = "PhoneNumber*" - sendButton.setTitle("SEND", for: .normal) - } - private func setUpKeyboard() { phoneNumberTextField.becomeFirstResponder() phoneNumberTextField.keyboardType = .numberPad @@ -186,7 +189,7 @@ final class PhoneNumberVerificationViewController: UIViewController { let output = viewModel.transform(loginPhase: LoginPhase.PhoneNumber, input: input) output - .buttonIsValid + .buttonIsValid? .sink(receiveValue: { [weak self] state in self?.sendButton.isEnabled = state self?.sendButton.setTitleColor(state ? .mainWhite : .mainBlack, for: .normal) @@ -214,15 +217,15 @@ final class PhoneNumberVerificationViewController: UIViewController { } @objc private func keyboardWillHide(_ notification: Notification) { - sendButtonBottomConstraints.constant = -16 + sendButtonBottomConstraints.constant = -Constraints.shared.space16 view.layoutIfNeeded() } } extension PhoneNumberVerificationViewController { func navigateToSMSCodeVerificationPage(phoneNumberString: String, smsCode: String) { - viewModel.setPhoneNumber(number: phoneNumberString) - viewModel.setSMSCode(number: smsCode) + viewModel.phoneNumber = phoneNumberString + viewModel.smsCode = smsCode self.navigationController?.pushViewController(SMSCodeVerificationViewController(viewModel: viewModel), animated: true) } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeInputView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeInputView.swift index 1abccfc..0fe4f88 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeInputView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeInputView.swift @@ -20,14 +20,20 @@ final class SMSCodeInputView: UIView { } } } - var smsCodeTextField = UITextField() + var smsCodeTextField: UITextField = { + let textField = UITextField() + textField.font = UIFont(weight: .regular, size: 29) + textField.textColor = UIColor(rgb: 0xAC6767) + textField.textAlignment = .center + textField.keyboardType = .numberPad + return textField + }() override init(frame: CGRect) { super.init(frame: frame) setUpConstraints() setUpStyle() - setUpKeyboard() } @@ -35,32 +41,24 @@ final class SMSCodeInputView: UIView { fatalError("init(coder:) has not been implemented") } - func setUpConstraints() { + private func setUpConstraints() { self.addSubview(smsCodeTextField) smsCodeTextField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ smsCodeTextField.centerXAnchor.constraint(equalTo: self.centerXAnchor), smsCodeTextField.centerYAnchor.constraint(equalTo: self.centerYAnchor), - smsCodeTextField.widthAnchor.constraint(equalToConstant: 16), + smsCodeTextField.widthAnchor.constraint(equalToConstant: 64), smsCodeTextField.heightAnchor.constraint(equalToConstant: 40), ]) } - func setUpStyle() { + private func setUpStyle() { self.backgroundColor = UIColor(rgb: 0xFBD6D6) self.layer.cornerRadius = 5 self.layer.borderColor = UIColor.mainBlack.cgColor self.layer.borderWidth = 0 - - smsCodeTextField.font = UIFont(weight: .regular, size: 29) - smsCodeTextField.textColor = UIColor(rgb: 0xAC6767) - smsCodeTextField.textAlignment = .center - } - - func setUpKeyboard() { - smsCodeTextField.keyboardType = .numberPad } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeVertificationViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeVertificationViewController.swift index ef5685a..f6ece9f 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeVertificationViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Login/View/SMSCodeVertificationViewController.swift @@ -9,6 +9,7 @@ import Combine import CombineCocoa import UIKit +// TODO: UI/UX Question final class SMSCodeVerificationViewController: UIViewController { private let authManager = AuthManager(service: APIManager()) @@ -16,7 +17,7 @@ final class SMSCodeVerificationViewController: UIViewController { let label = UILabel() label.font = UIFont(weight: .bold, size: 24) label.textColor = .mainBlack - label.text = "Enter Code" + label.text = "code_ins_txt".localized() return label }() @@ -24,7 +25,7 @@ final class SMSCodeVerificationViewController: UIViewController { let label = UILabel() label.font = UIFont(weight: .regular, size: 14) label.textColor = .mainBlack - label.text = "An SMS code was sent to" + label.text = "code_des_txt".localized() return label }() @@ -33,7 +34,7 @@ final class SMSCodeVerificationViewController: UIViewController { label.font = UIFont(weight: .bold, size: 16) label.textAlignment = .left label.textColor = .mainBlack - label.text = self.viewModel.getPhoneNumber() + label.text = "+82 \(self.viewModel.phoneNumber)" return label }() @@ -47,7 +48,8 @@ final class SMSCodeVerificationViewController: UIViewController { label.font = UIFont(weight: .regular, size: 14) label.textAlignment = .right label.textColor = .mainRed - label.text = "Not receiveing the code?" + label.text = "code_resend_ins_txt".localized() + label.isUserInteractionEnabled = true return label }() @@ -58,7 +60,7 @@ final class SMSCodeVerificationViewController: UIViewController { button.backgroundColor = .mainLightGray button.layer.cornerRadius = 27.5 button.isUserInteractionEnabled = false - button.setTitle("CONFIRM", for: .normal) + button.setTitle("confirm".localized(), for: .normal) return button }() @@ -90,25 +92,22 @@ final class SMSCodeVerificationViewController: UIViewController { setUpConstraints() setUpStyle() + setUpAction() setUpLayerName() - setUpDelegate() setUpKeyboard() bind(viewModel: viewModel) } private func setUpConstraints() { - let space4: CGFloat = 4 - let space8: CGFloat = 8 - let space16: CGFloat = 16 - + let make = Constraints.shared let safeArea = view.safeAreaLayoutGuide let smsCodeInputStackView = UIStackView() smsCodeInputStackView.axis = NSLayoutConstraint.Axis.horizontal smsCodeInputStackView.distribution = UIStackView.Distribution.equalSpacing smsCodeInputStackView.alignment = UIStackView.Alignment.center - smsCodeInputStackView.spacing = 12 + smsCodeInputStackView.spacing = make.space12 [ instructionLabel, @@ -123,23 +122,23 @@ final class SMSCodeVerificationViewController: UIViewController { }) NSLayoutConstraint.activate([ - instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: space16), - instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + instructionLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: make.space16), + instructionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + instructionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), instructionLabel.heightAnchor.constraint(equalToConstant: 32) ]) NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: space4), - descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: space16), + descriptionLabel.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: make.space4), + descriptionLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + descriptionLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: make.space16), descriptionLabel.heightAnchor.constraint(equalToConstant: 22) ]) NSLayoutConstraint.activate([ - phoneNumberLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: space4), - phoneNumberLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - phoneNumberLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + phoneNumberLabel.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: make.space4), + phoneNumberLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + phoneNumberLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), phoneNumberLabel.heightAnchor.constraint(equalToConstant: 22) ]) @@ -147,9 +146,9 @@ final class SMSCodeVerificationViewController: UIViewController { smsCodeInputStackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - smsCodeInputStackView.topAnchor.constraint(equalTo: phoneNumberLabel.bottomAnchor, constant: space16), - smsCodeInputStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - smsCodeInputStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + smsCodeInputStackView.topAnchor.constraint(equalTo: phoneNumberLabel.bottomAnchor, constant: make.space16), + smsCodeInputStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + smsCodeInputStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), smsCodeInputStackView.heightAnchor.constraint(equalToConstant: 54) ]) @@ -170,17 +169,17 @@ final class SMSCodeVerificationViewController: UIViewController { }) NSLayoutConstraint.activate([ - codeResendLabel.topAnchor.constraint(equalTo: smsCodeInputStackView.bottomAnchor, constant: space8), - codeResendLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + codeResendLabel.topAnchor.constraint(equalTo: smsCodeInputStackView.bottomAnchor, constant: make.space8), + codeResendLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), codeResendLabel.widthAnchor.constraint(equalToConstant: 300), codeResendLabel.heightAnchor.constraint(equalToConstant: 24), ]) - confirmButtonBottomConstraints = confirmButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -space16) + confirmButtonBottomConstraints = confirmButton.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -make.space16) NSLayoutConstraint.activate([ - confirmButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: space16), - confirmButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -space16), + confirmButton.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: make.space16), + confirmButton.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -make.space16), confirmButtonBottomConstraints, confirmButton.heightAnchor.constraint(equalToConstant: 55) ]) @@ -197,12 +196,6 @@ final class SMSCodeVerificationViewController: UIViewController { } } - private func setUpDelegate() { - [smsCodeInputView1, smsCodeInputView2, smsCodeInputView3, smsCodeInputView4].forEach({ - $0.smsCodeTextField.delegate = self - }) - } - private func setUpKeyboard() { smsCodeInputView1.smsCodeTextField.becomeFirstResponder() NotificationCenter.default.addObserver(self, selector:#selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) @@ -214,14 +207,26 @@ final class SMSCodeVerificationViewController: UIViewController { let smsCodeViews = [smsCodeInputView1, smsCodeInputView2, smsCodeInputView3, smsCodeInputView4] for index in 0...3 { + + smsCodeViews[index].smsCodeTextField.controlEventPublisher(for: .editingDidBegin).sink { + let textField = smsCodeViews[index].smsCodeTextField + textField.text = "" + guard let textFieldLayerName = textField.layer.name else { return } + guard let index = Int(textFieldLayerName) else { return } + self.smsCodeCheckArr[index] = false + } + .store(in: &cancellables) + smsCodeViews[index].smsCodeTextField.textPublisher.sink { - if $0.count == 1 { + + guard let textLength = $0?.count else { return } + if textLength == 1 { if index != 3 { smsCodeViews[(index+1)].smsCodeTextField.becomeFirstResponder() smsCodeViews[(index+1)].smsCodeTextField.text = "" } self.smsCodeCheckArr[index] = true - } else if $0.count > 1 && index == 3 { + } else if textLength > 1 && index == 3 { smsCodeViews[(index)].smsCodeTextField.text?.removeFirst() } } @@ -231,6 +236,7 @@ final class SMSCodeVerificationViewController: UIViewController { confirmButton.tapPublisher.sink { [self] in Task { if smsCodeVerify() { + // MARK: 회원가입 관련 userVerify 메소드는 NicknameVC에서 호출 예정 let isUser = try await viewModel.userVerify() print("USER CHECK: ", isUser) if isUser { @@ -248,6 +254,18 @@ final class SMSCodeVerificationViewController: UIViewController { }.store(in: &cancellables) } + private func setUpAction() { + let tapGesture = UITapGestureRecognizer() + + codeResendLabel.addGestureRecognizer(tapGesture) + tapGesture.tapPublisher.sink { [weak self] _ in + Task { + guard let phoneNumber = self?.viewModel.phoneNumber else { return } + _ = try await self?.viewModel.phoneNumberVerify(phoneNumber: phoneNumber) + } + }.store(in: &cancellables) + } + @objc private func keyboardWillShow(_ notification: Notification) { if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardHeight = keyboardFrame.cgRectValue.height @@ -258,26 +276,16 @@ final class SMSCodeVerificationViewController: UIViewController { } @objc private func keyboardWillHide(_ notification: Notification) { - confirmButtonBottomConstraints.constant = -16 + confirmButtonBottomConstraints.constant = Constraints.shared.space16 view.layoutIfNeeded() } } -extension SMSCodeVerificationViewController: UITextFieldDelegate { - func textFieldDidBeginEditing(_ textField: UITextField) { - textField.text = "" - guard let textFieldLayerName = textField.layer.name else { return } - guard let index = Int(textFieldLayerName) else { return } - self.smsCodeCheckArr[index] = false - - } -} - extension SMSCodeVerificationViewController { func smsCodeVerify() -> Bool { let userInput = [smsCodeInputView1, smsCodeInputView2, smsCodeInputView3, smsCodeInputView4] .compactMap{$0.smsCodeTextField.text} .reduce("") { return $0 + $1 } - return userInput == viewModel.getSMSCode() + return userInput == viewModel.smsCode } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Main/TabBarViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Main/TabBarViewController.swift index 21f5e24..c0db7f5 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Main/TabBarViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Main/TabBarViewController.swift @@ -20,7 +20,7 @@ final class TabBarViewController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() - + NotificationCenter.default.addObserver(self, selector: #selector(showCallerPage), name: NSNotification.Name("ShowCallerPage"), object: nil) setUpTabBar() } @@ -29,11 +29,11 @@ final class TabBarViewController: UITabBarController { let educationVC = EducationMainViewController(viewModel: EducationViewModel()) let callVC = CallMainViewController(viewModel: CallViewModel()) - let mypageVC = MypageViewController(viewModel: EducationViewModel()) + let mypageVC = MypageViewController(authViewModel: AuthViewModel(), eduViewModel: EducationViewModel()) - educationVC.title = "Education" - callVC.title = "Call" - mypageVC.title = "Profile" + educationVC.title = "edu_tab_t".localized() + callVC.title = "call_tab_t".localized() + mypageVC.title = "mypage_tab_t".localized() educationVC.tabBarItem.image = UIImage.init(systemName: "book") callVC.tabBarItem.image = UIImage.init(systemName: "bell") @@ -53,4 +53,16 @@ final class TabBarViewController: UITabBarController { setViewControllers([navigationEdu, navigationCall, navigationMypage], animated: false) } + + @objc func showCallerPage(_ notification:Notification) { + if let userInfo = notification.userInfo { + guard let type = userInfo["type"] as? String else { return } + if type == "1" { + if self.selectedIndex != 1 { + self.selectedIndex = 1 + } + } + } + } + } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageStatusView.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageStatusView.swift index 217c57c..16ae77f 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageStatusView.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageStatusView.swift @@ -133,8 +133,6 @@ final class MypageStatusView: UIView { private func setUpStyle() { self.layer.cornerRadius = 16 - self.layer.borderColor = UIColor.mainRed.cgColor - self.layer.borderWidth = 1 self.backgroundColor = .mainWhite } @@ -144,10 +142,10 @@ final class MypageStatusView: UIView { var statusText: String = "" var leftDay: Int = 0 if let day = certificate.leftDay { - statusText = "\(certificate.status.certificationStatus()) (D-\(day))" + statusText = "\(certificate.status.getStatus()) (D-\(day))" leftDay = day } else { - statusText = "\(certificate.status.certificationStatus())" + statusText = "\(certificate.status.getStatus())" } angelStatusImageView.image = UIImage(named: imgName) @@ -155,11 +153,13 @@ final class MypageStatusView: UIView { [periodLabel, progressView, expirationLabel].forEach({$0.isHidden = (certificate.status != .acquired) }) - progressView.progress = Float(leftDay)/90 + progressView.progress = 1 - Float(leftDay)/90 expirationLabel.text = leftDay.numberAsExpirationDate() } func setUpGreetingLabel(nickname: String) { - nicknameLabel.text = "Hi \(nickname)\nYour Certification Status is" + + let localizedStr = String(format: "%@_greet_des_txt".localized(), nickname) + nicknameLabel.text = localizedStr } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageViewController.swift b/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageViewController.swift index 092421e..b66132e 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageViewController.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Scene/Mypage/MypageViewController.swift @@ -11,7 +11,7 @@ import UIKit final class MypageViewController: UIViewController { private lazy var mypageStatusView: MypageStatusView = { - let view = MypageStatusView(viewModel: viewModel) + let view = MypageStatusView(viewModel: eduViewModel) return view }() @@ -19,20 +19,21 @@ final class MypageViewController: UIViewController { let view = UITableView(frame: CGRect.zero, style: .insetGrouped) view.backgroundColor = .white view.sectionHeaderTopPadding = 0 - view.isScrollEnabled = false view.showsVerticalScrollIndicator = false return view }() private var statusViewBottomAnchor: NSLayoutConstraint? - private var viewModel: EducationViewModel + private var authViewModel: AuthViewModel + private var eduViewModel: EducationViewModel private var cancellables = Set() let sectionHeader = ["History", "etc", ""] var cellDataSource = [["Dispatch History", "Call History"], ["Developer Information", "Liscence"], ["Logout"]] - init(viewModel: EducationViewModel) { - self.viewModel = viewModel + init(authViewModel: AuthViewModel, eduViewModel: EducationViewModel) { + self.authViewModel = authViewModel + self.eduViewModel = eduViewModel super.init(nibName: nil, bundle: nil) } @@ -46,7 +47,7 @@ final class MypageViewController: UIViewController { setUpConstraints() setUpStyle() setUpTableView() - bind(viewModel: viewModel) + bind(viewModel: eduViewModel) } private func setUpConstraints() { @@ -68,8 +69,8 @@ final class MypageViewController: UIViewController { NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: mypageStatusView.bottomAnchor), - tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - tableView.widthAnchor.constraint(equalToConstant: 358), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.heightAnchor.constraint(equalToConstant: 390) ]) } @@ -144,4 +145,38 @@ extension MypageViewController: UITableViewDelegate, UITableViewDataSource { return cell } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == 2 && indexPath.row == 0 { // logout button + showLogOutAlert() + } + } + + func showLogOutAlert() { + let alert = UIAlertController(title: "logout".localized(), message: "logout_des_txt".localized(), preferredStyle: .alert) + + let confirm = UIAlertAction(title: "yes".localized(), style: .destructive, handler: { _ in + Task { [weak self] in + let isSuccess = try await self?.authViewModel.logOut() + if isSuccess == true { + guard let window = self?.view.window else { return } + await window.setRootViewController(AutoLoginViewController(), animated: true) + UserDefaultsManager.accessToken = "" + UserDefaultsManager.refreshToken = "" + UserDefaultsManager.isCertificateNotice = false + self?.dismiss(animated: true) + } + } + }) + let cancel = UIAlertAction(title: "no".localized(), style: .cancel, handler: { [weak self] _ in + guard let indexPath = self?.tableView.indexPathForSelectedRow else { return } + self?.tableView.deselectRow(at: indexPath, animated: true) + }) + + [confirm, cancel].forEach { + alert.addAction($0) + } + + present(alert, animated: true, completion: nil) + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Service/APIServices/Requestable.swift b/CPR2U-iOS/CPR2U/CPR2U/Service/APIServices/Requestable.swift index c299d62..46f7cbb 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Service/APIServices/Requestable.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Service/APIServices/Requestable.swift @@ -28,8 +28,6 @@ final class APIManager: Requestable { let decodedData = try JSONDecoder().decode(NetworkResponse.self, from: data) - // MARK: TEST CODE - print("MSG: ", decodedData.message) if decodedData.status == 200 { return (true, decodedData.data) } else { diff --git a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressEndPoint.swift b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressEndPoint.swift index ab0d84c..c4fcf20 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressEndPoint.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressEndPoint.swift @@ -8,7 +8,6 @@ import Foundation enum AddressEndPoint { - case getAddressList case setUserAddress(id: Int) } @@ -18,16 +17,12 @@ extension AddressEndPoint: EndPoint { switch self { case .setUserAddress: return .POST - case .getAddressList: - return .GET } } var body: Data? { var params: [String : Int] switch self { - case .getAddressList: - return nil case .setUserAddress(let id): params = ["address_id": id] } @@ -38,8 +33,6 @@ extension AddressEndPoint: EndPoint { func getURL(path: String) -> String { let baseURL = URLs.baseURL switch self { - case .getAddressList: - return "\(baseURL)/users/address" case .setUserAddress: return "\(baseURL)/users/address" } @@ -51,10 +44,6 @@ extension AddressEndPoint: EndPoint { headers["Authorization"] = UserDefaultsManager.accessToken switch self { - case .getAddressList: - return NetworkRequest(url: getURL(path: baseURL), - httpMethod: method, - headers: headers) case .setUserAddress: headers["Content-Type"] = "application/json" return NetworkRequest(url: getURL(path: baseURL), diff --git a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressManager.swift b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressManager.swift index a7d84dc..270f7e1 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressManager.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Address/AddressManager.swift @@ -8,7 +8,6 @@ import Foundation protocol AddressService { - func getAddressList() async throws -> (success: Bool, data: [AddressListResult]?) func setUserAddress(id: Int) async throws -> (success: Bool, data: SetUserAddressResult?) } @@ -20,13 +19,6 @@ struct AddressManager: AddressService { self.service = service } - func getAddressList() async throws -> (success: Bool, data: [AddressListResult]?) { - let request = AddressEndPoint - .getAddressList - .createRequest() - return try await self.service.request(request) - } - func setUserAddress(id: Int) async throws -> (success: Bool, data: SetUserAddressResult?) { let request = AddressEndPoint .setUserAddress(id: id) diff --git a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthEndPoint.swift b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthEndPoint.swift index d51f103..ef5ada5 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthEndPoint.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthEndPoint.swift @@ -7,38 +7,57 @@ import Foundation +struct AnyEncodable: Encodable { + private let encodable: Encodable + + init(_ encodable: Encodable) { + self.encodable = encodable + } + + func encode(to encoder: Encoder) throws { + try encodable.encode(to: encoder) + } +} + enum AuthEndPoint { case phoneNumberVerify (phoneNumber: String) case nicknameVerify (nickname: String) case signIn (phoneNumber: String, deviceToken: String) - case signUp (nickname: String, phoneNumber: String, deviceToken: String) + case signUp (nickname: String, phoneNumber: String, addressId: Int, deviceToken: String) case autoLogin (refreshToken: String) + case logOut + case getAddressList } extension AuthEndPoint: EndPoint { var method: HttpMethod { switch self { - case .phoneNumberVerify, .signIn, .signUp, .autoLogin: + case .phoneNumberVerify, .signIn, .signUp, .autoLogin, .logOut: return .POST - case .nicknameVerify: + case .nicknameVerify, .getAddressList: return .GET } } var body: Data? { - var params: [String : String] + var params: [String : AnyEncodable] switch self { case .phoneNumberVerify(let phoneNumber): - params = ["phone_number" : phoneNumber] + params = ["phone_number" : AnyEncodable(phoneNumber)] case .nicknameVerify(let nickname): - params = ["nickname" : nickname ] + params = ["nickname" : AnyEncodable(nickname)] case .signIn(let phoneNumber, let deviceToken): - params = [ "phone_number" : phoneNumber, "device_token" : deviceToken ] - case .signUp(let nickname, let phoneNumber, let deviceToken): - params = ["nickname" : nickname, "phone_number" : phoneNumber, "device_token" : deviceToken ] + params = [ "phone_number" : AnyEncodable(phoneNumber), "device_token" : AnyEncodable(deviceToken)] + case .signUp(let nickname, let phoneNumber, let addressId, let deviceToken): + params = ["nickname" : AnyEncodable(nickname), "phone_number" : AnyEncodable(phoneNumber), "address_id" : + AnyEncodable(addressId), "device_token" : AnyEncodable(deviceToken)] case .autoLogin(let refreshToken) : - params = ["refresh_token" : refreshToken] + params = ["refresh_token" : AnyEncodable(refreshToken)] + case .getAddressList: + return nil + case .logOut : + return nil } return params.encode() @@ -57,6 +76,10 @@ extension AuthEndPoint: EndPoint { return "\(baseURL)/auth/signup" case .autoLogin: return "\(baseURL)/auth/auto-login" + case .getAddressList: + return "\(baseURL)/auth/address" + case .logOut: + return "\(baseURL)/auth/logout" } } @@ -97,6 +120,13 @@ extension AuthEndPoint: EndPoint { httpMethod: method, headers: headers, requestBody: body) + case .logOut: + var headers: [String: String] = [:] + headers["Authorization"] = UserDefaultsManager.accessToken + return NetworkRequest(url: getURL(path: baseURL), httpMethod: method, headers: headers) + case .getAddressList: + return NetworkRequest(url: getURL(path: baseURL), + httpMethod: method) } } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthManager.swift b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthManager.swift index 0789798..e760acb 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthManager.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Auth/AuthManager.swift @@ -11,8 +11,10 @@ protocol AuthService { func phoneNumberVerify(phoneNumber: String) async throws -> (success: Bool, data: SMSCodeResult?) func nicknameVerify(nickname: String) async throws -> (success: Bool, data: NicknameVerifyResult?) func signIn(phoneNumber: String, deviceToken: String) async throws -> (success: Bool, data: SignInResult?) - func signUp(nickname: String, phoneNumber: String, deviceToken: String) async throws -> (success: Bool, data: SignUpResult?) + func signUp(nickname: String, phoneNumber: String, addressId: Int, deviceToken: String) async throws -> (success: Bool, data: SignUpResult?) func autoLogin(refreshToken: String) async throws -> (success: Bool, data: AutoLoginResult?) + func logOut() async throws -> (success: Bool, data: LogOutResult?) + func getAddressList() async throws -> (success: Bool, data: [AddressListResult]?) } struct AuthManager: AuthService { @@ -44,9 +46,9 @@ struct AuthManager: AuthService { return try await self.service.request(request) } - func signUp(nickname: String, phoneNumber: String, deviceToken: String) async throws -> (success: Bool, data: SignUpResult?) { + func signUp(nickname: String, phoneNumber: String, addressId: Int, deviceToken: String) async throws -> (success: Bool, data: SignUpResult?) { let request = AuthEndPoint - .signUp(nickname: nickname, phoneNumber: phoneNumber, deviceToken: deviceToken) + .signUp(nickname: nickname, phoneNumber: phoneNumber, addressId: addressId, deviceToken: deviceToken) .createRequest() return try await self.service.request(request) } @@ -57,4 +59,18 @@ struct AuthManager: AuthService { .createRequest() return try await self.service.request(request) } + + func getAddressList() async throws -> (success: Bool, data: [AddressListResult]?) { + let request = AuthEndPoint + .getAddressList + .createRequest() + return try await self.service.request(request) + } + + func logOut() async throws -> (success: Bool, data: LogOutResult?) { + let request = AuthEndPoint + .logOut + .createRequest() + return try await self.service.request(request) + } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Dispatch/DispatchViewModel.swift b/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Dispatch/DispatchViewModel.swift deleted file mode 100644 index 80c683b..0000000 --- a/CPR2U-iOS/CPR2U/CPR2U/Service/Network/Dispatch/DispatchViewModel.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// DispatchViewModel.swift -// CPR2U -// -// Created by 황정현 on 2023/03/31. -// - -import Foundation - -//class DispatchViewModel: OutputOnlyViewModelType { -// struct Output = { -// -// } -// -// -//} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/String+.swift b/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/String+.swift new file mode 100644 index 0000000..13b1130 --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/String+.swift @@ -0,0 +1,21 @@ +// +// String+.swift +// CPR2U +// +// Created by 황정현 on 2023/05/11. +// + +import Foundation + +extension String { + func localized() -> String { + return NSLocalizedString(self, comment: "") + } + + func elapsedTime() -> Int { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy'-'MM'-'dd' 'HH':'mm':'ss" + guard let date = dateFormatter.date(from: self) else { return 0 } + return Int(-date.timeIntervalSinceNow) + } +} diff --git a/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UITextField+.swift b/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UITextField+.swift index b997a34..edc370c 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UITextField+.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UITextField+.swift @@ -2,17 +2,25 @@ // UITextField+.swift // CPR2U // -// Created by 황정현 on 2023/03/06. -// +// Created by 황정현 on 2023/05/24. +// https://stackoverflow.com/questions/2694411/text-inset-for-uitextfield import UIKit -import Combine -extension UITextField { - var textPublisher: AnyPublisher { - controlPublisher(for: .editingChanged) - .map { $0 as! UITextField } - .map { $0.text! } - .eraseToAnyPublisher() +class TextField: UITextField { + let inset: CGFloat = 10 + + // placeholder position + override func textRect(forBounds: CGRect) -> CGRect { + return forBounds.insetBy(dx: self.inset , dy: self.inset) + } + + // text position + override func editingRect(forBounds: CGRect) -> CGRect { + return forBounds.insetBy(dx: self.inset , dy: self.inset) + } + + override func placeholderRect(forBounds: CGRect) -> CGRect { + return forBounds.insetBy(dx: self.inset, dy: self.inset) } } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UIWindow+.swift b/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UIWindow+.swift index 8310e23..b3a2501 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UIWindow+.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Util/Extension/UIWindow+.swift @@ -27,5 +27,4 @@ extension UIWindow { } }) } - } diff --git a/CPR2U-iOS/CPR2U/CPR2U/Util/Protocol/ViewModelProtocol.swift b/CPR2U-iOS/CPR2U/CPR2U/Util/Protocol/ViewModelProtocol.swift index b2ed110..95e6991 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Util/Protocol/ViewModelProtocol.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Util/Protocol/ViewModelProtocol.swift @@ -7,6 +7,13 @@ import Foundation +protocol AuthViewModelType { + associatedtype Input + associatedtype Output + + func transform(loginPhase: LoginPhase, input: Input) -> Output +} + protocol DefaultViewModelType { associatedtype Input associatedtype Output diff --git a/CPR2U-iOS/CPR2U/CPR2U/Util/Wrapper/UserDefaultsManager.swift b/CPR2U-iOS/CPR2U/CPR2U/Util/Wrapper/UserDefaultsManager.swift index 506b0ef..b722e7a 100644 --- a/CPR2U-iOS/CPR2U/CPR2U/Util/Wrapper/UserDefaultsManager.swift +++ b/CPR2U-iOS/CPR2U/CPR2U/Util/Wrapper/UserDefaultsManager.swift @@ -36,7 +36,4 @@ struct UserDefaultsManager { @UserDefaultWrapper(key: "isCertificateNotice", defaultValue: false) static var isCertificateNotice - - @UserDefaultWrapper(key: "isAddressSet", defaultValue: false) - static var isAddressSet } diff --git a/CPR2U-iOS/CPR2U/CPR2U/en.lproj/Localizable.strings b/CPR2U-iOS/CPR2U/CPR2U/en.lproj/Localizable.strings new file mode 100644 index 0000000..ced6ea4 --- /dev/null +++ b/CPR2U-iOS/CPR2U/CPR2U/en.lproj/Localizable.strings @@ -0,0 +1,148 @@ +/* + Localizable.strings + CPR2U + + Created by 황정현 on 2023/05/11. + +*/ + +/* +key의 길이가 길어지는 것을 방지하기 위해 아래의 규칙에 의거하여 key 이름을 지정합니다. +education: edu +call: call +mypage: mpg +tab: tab +t: title +txt: text +ins: instruction +des: description +ann: annotation +warn: warning +pn: phone number +phdr: placeholder + +*/ + +// Common +"send" = "SEND"; +"confirm" = "CONFIRM"; +"continue" = "CONTINUE"; +"cancel" = "CANCEL"; +"submit" = "SUBMIT"; +"quit" = "QUIT"; +"logout" = "LOGOUT"; +"yes" = "YES"; +"no" = "NO"; +"got_it" = "GOT IT"; +"completed" = "COMPLETED!"; +"completed_not" = "Not Completed"; +"open_not" = "Not Opened"; +"%@_greeting_txt" = "Hi %@"; +"%@_greet_des_txt" = "Hi %@ nYour Certification Status is"; + +// TabBarView +"edu_tab_t" = "Education"; +"call_tab_t" = "Call"; +"mypage_tab_t" = "Profile"; + +// Angel Status +"cert_status_ann_txt" = "Certificate Status: "; +"angel_progress_ann_txt" = "CPR Angel Certification Progress"; +"acq_status" = "ACQUIRED"; +"exp_status" = "EXPIRED"; +"unacq_status" = "UNACQUIRED"; + +// PhoneNumberVC +"nation_code" = "+ 82"; +"pn_ins_txt" = "Enter your number"; +"pn_des_txt" = "We will send a code to verify your mobile number"; +"pn_phdr" = "PhoneNumber*"; + +// SMSCodeVerificationVC +"code_ins_txt"= "Enter Code"; +"code_des_txt"= "An SMS code was sent to"; +"code_resend_ins_txt" = "Not receiveing the code?"; + +// NicknameVerificationVC +"nickname_special_character" = "Nickname cannot contain special characters"; +"%@_nickname_unavailable" = "%@ is Unavailable"; +"nickname_ins_txt" = "Enter your Nickname"; +"nickname_des_txt" = "People can recognize you by your nickname"; +"nickname_phdr" = "Nickname*"; + +// AddressVerificationVC +//"address_des_txt"= "Select your address for\nCPR Angel activities"; +"address_ins_txt" = "Select your Address"; +"address_des_txt" = "Select your address for CPR Angel activities"; + +// NoticeV +// - titleLabels +//case quizFail +//case quizPass +//case certificate +//case dispatchComplete +"lecture_pass_title_txt" = "Lecture Complete!"; +"quiz_fail_title_txt_%@" = "Quiz Failed: %@"; +"quiz_pass_title_txt" = "Quiz Passed!"; +"certificate_title_txt" = "Congratulations!"; +"dispatchComplete_title_txt" = "Completed!"; +"lecture_pass_des_txt" = "You have completed the Lecture!\nYou are one step closer to becoming\na CPR Angel!"; +"quiz_fail_des_txt" = "You did not complete the Quiz.\nPlease try again."; +"quiz_pass_des_txt" = "You have completed the Quiz!\nYou are one step closer to becoming\na CPR Angel!"; +"certificate_des_txt" = "You have got CPR Angel Certificate!\nNow you're a CPR Angel and can help\nsomeone in cardiac arrest."; +"dispatchComplete_des_txt" = "You have arrived in the emergency.\nPlease help the caller!"; + +// Education +"course_lec" = "Lecture"; +"course_quiz" = "Quiz"; +"course_pose" = "Pose Practice"; +"course_lec_des" = "Video lecture for CPR angel certificate"; +"course_quiz_des" = "Let’s check your CPR study"; +"course_pose_des" = "Posture practice to get CPR angel certificate"; +"taken_%dtime_des_txt" = "Takes about %d minutes"; + +// Education - Quiz +"quiz_exit" = "Quiz Exit"; +"quiz_exit_warn_txt" = "All progress will be lost"; + +// Education - Pose +"your_result_txt" = "YOUR RESULT"; +"pass" = "PASSED!"; +"fail" = "FAILED..."; +"pe_title_txt_1" = "Prepare tools"; +"pe_title_txt_2" = "Prepare tools"; +"pe_title_txt_3" = "Draw an angry man"; +"pe_title_txt_4" = "Ready"; +"pe_des_txt_1" = "If you do not have a CPR mannequin,\nplease prepare a plastic bottle, pillow, etc."; +"pe_des_txt_2" = "Put the plastic bottle inside the clothes\nyou don't wear and wrap it up."; +"pe_des_txt_3" = "Draw an angry man on your clothes or pillow\nusing tape or pen."; +"pe_des_txt_4" = "Please press the location marked in red!"; +"pe_ins_txt" = "Please assume the position"; +"pe_count_des_txt" = "Time Until Posture Measurement Begins"; + +// Call +"approch_des_txt" = "Your call has been\nsent to the CPR Angels\nnear you."; +"angel_approach_notice_default_txt" = "Calling CPR Angels..."; +"angel_approach_notice_%dmatched_txt" = "%d CPR Angels Matched!\nIt’s coming this way."; +"siuation_end_des_txt" = "SITUATION ENDED"; +"call_ins_txt" = "Call 911"; +"call_des_txt" = "Calling 911 is the first priority. Ask the people around you to report or perform CPR after reporting. If the report is false, you will be restricted from using the app."; +"pe_pass_txt" = "Congratulations!\nYou passed the CPR posture test."; +"pe_fail_txt" = "Failed.\nYou need to practice CPR posture more."; + + +// Dispatch +"elapsed_time_txt" = "Elapsed Time"; +"patient_loc_txt" = "Patient Location"; +"dispatch_ins_txt" = "Shall we start out?"; +"dispatch_alert_txt" = "If you arrive within 5 minutes,\nthe patient's survival rate increases."; +"dispatch_des_txt" = "When you arrive at your destination,\ndispatch ends automatically."; +"dispatch_tab_t" = "DISPATCH"; +"arrive_des_txt" = "ARRIVED"; +"report_title_txt" = "Is there any problem? Report us"; +"report_ins_txt" = "What are you trying to report?"; +"report_des_txt" = "Your report will be treated anonymously."; +"report_phdr" = "Content*"; + +// Auth +"logout_des_txt" = "Are you sure you want to Logout?";