diff --git a/HongikYeolgong2.xcodeproj/project.pbxproj b/HongikYeolgong2.xcodeproj/project.pbxproj index 207f114..7db38a1 100644 --- a/HongikYeolgong2.xcodeproj/project.pbxproj +++ b/HongikYeolgong2.xcodeproj/project.pbxproj @@ -43,7 +43,7 @@ 4763FFBA2CB913FF00990336 /* UserDataInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4763FFB92CB913FF00990336 /* UserDataInteractor.swift */; }; 4763FFBC2CB9228B00990336 /* AppEnviroment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4763FFBB2CB9228B00990336 /* AppEnviroment.swift */; }; 47860E092CB82BEF005701C0 /* TimePickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47860E082CB82BEF005701C0 /* TimePickerViewModel.swift */; }; - 478933792CADBDA500E1D89E /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478933782CADBDA500E1D89E /* SignInView.swift */; }; + 478933792CADBDA500E1D89E /* SignUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 478933782CADBDA500E1D89E /* SignUpView.swift */; }; 4789337B2CADC43D00E1D89E /* DropDownPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4789337A2CADC43D00E1D89E /* DropDownPicker.swift */; }; 4795DC902CB43B2700EC3AB1 /* TimePickerDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4795DC8F2CB43B2700EC3AB1 /* TimePickerDialog.swift */; }; 479821D42CA24CFF002357EB /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 479821D32CA24CFF002357EB /* .swiftlint.yml */; }; @@ -76,10 +76,18 @@ 47B1D4C72C9CB1760071B62B /* HongikYeolgong2UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B1D4C62C9CB1760071B62B /* HongikYeolgong2UITests.swift */; }; 47B1D4C92C9CB1760071B62B /* HongikYeolgong2UITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B1D4C82C9CB1760071B62B /* HongikYeolgong2UITestsLaunchTests.swift */; }; 47BACCF72CA164BA00295DAC /* Font+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BACCF62CA164BA00295DAC /* Font+.swift */; }; + 47BE30E32CC813BB0015D973 /* KeyChainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BE30E22CC813BB0015D973 /* KeyChainManager.swift */; }; + 47BE30E52CC81A9E0015D973 /* URLRequest+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47BE30E42CC81A9E0015D973 /* URLRequest+.swift */; }; 47CA17252CC9336100CBB251 /* StudyPeriodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CA17242CC9336100CBB251 /* StudyPeriodView.swift */; }; 47CA17272CC9340800CBB251 /* StudyTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47CA17262CC9340800CBB251 /* StudyTimerView.swift */; }; 47D6662C2CB4499300813ECA /* TimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D6662B2CB4499300813ECA /* TimePicker.swift */; }; 47DF041F2CBE7A29007E58A7 /* Encodable+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DF041E2CBE7A29007E58A7 /* Encodable+.swift */; }; + 47F4F6972CC88FBB00543D24 /* SignUpRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F4F6962CC88FBB00543D24 /* SignUpRequestDTO.swift */; }; + 47F4F6992CC89A6900543D24 /* SignUpResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F4F6982CC89A6900543D24 /* SignUpResponseDTO.swift */; }; + 47F4F69B2CC8E24300543D24 /* ServicesContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F4F69A2CC8E24300543D24 /* ServicesContainer.swift */; }; + 47F634FE2CC3E8A40034EAA9 /* NicknameCheckDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F634FD2CC3E8A40034EAA9 /* NicknameCheckDTO.swift */; }; + 47F635022CC3E98D0034EAA9 /* UserEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F635012CC3E98D0034EAA9 /* UserEndpoint.swift */; }; + 47F635052CC3EC1A0034EAA9 /* Department.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F635042CC3EC1A0034EAA9 /* Department.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -139,7 +147,7 @@ 4763FFB92CB913FF00990336 /* UserDataInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataInteractor.swift; sourceTree = ""; }; 4763FFBB2CB9228B00990336 /* AppEnviroment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnviroment.swift; sourceTree = ""; }; 47860E082CB82BEF005701C0 /* TimePickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerViewModel.swift; sourceTree = ""; }; - 478933782CADBDA500E1D89E /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; + 478933782CADBDA500E1D89E /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; 4789337A2CADC43D00E1D89E /* DropDownPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownPicker.swift; sourceTree = ""; }; 4795DC8F2CB43B2700EC3AB1 /* TimePickerDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerDialog.swift; sourceTree = ""; }; 479821D32CA24CFF002357EB /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; @@ -177,10 +185,18 @@ 47B1D4C62C9CB1760071B62B /* HongikYeolgong2UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HongikYeolgong2UITests.swift; sourceTree = ""; }; 47B1D4C82C9CB1760071B62B /* HongikYeolgong2UITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HongikYeolgong2UITestsLaunchTests.swift; sourceTree = ""; }; 47BACCF62CA164BA00295DAC /* Font+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+.swift"; sourceTree = ""; }; + 47BE30E22CC813BB0015D973 /* KeyChainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChainManager.swift; sourceTree = ""; }; + 47BE30E42CC81A9E0015D973 /* URLRequest+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+.swift"; sourceTree = ""; }; 47CA17242CC9336100CBB251 /* StudyPeriodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyPeriodView.swift; sourceTree = ""; }; 47CA17262CC9340800CBB251 /* StudyTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyTimerView.swift; sourceTree = ""; }; 47D6662B2CB4499300813ECA /* TimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePicker.swift; sourceTree = ""; }; 47DF041E2CBE7A29007E58A7 /* Encodable+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+.swift"; sourceTree = ""; }; + 47F4F6962CC88FBB00543D24 /* SignUpRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRequestDTO.swift; sourceTree = ""; }; + 47F4F6982CC89A6900543D24 /* SignUpResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpResponseDTO.swift; sourceTree = ""; }; + 47F4F69A2CC8E24300543D24 /* ServicesContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesContainer.swift; sourceTree = ""; }; + 47F634FD2CC3E8A40034EAA9 /* NicknameCheckDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameCheckDTO.swift; sourceTree = ""; }; + 47F635012CC3E98D0034EAA9 /* UserEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEndpoint.swift; sourceTree = ""; }; + 47F635042CC3EC1A0034EAA9 /* Department.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Department.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -221,6 +237,7 @@ isa = PBXGroup; children = ( 470723002CBC16680046469F /* AuthEndpoint.swift */, + 47F635012CC3E98D0034EAA9 /* UserEndpoint.swift */, ); path = Endpoints; sourceTree = ""; @@ -295,12 +312,12 @@ 4763FFB62CB9133600990336 /* Util */ = { isa = PBXGroup; children = ( + 47F4F69E2CC8E45A00543D24 /* Services */, 47DF041D2CBE7A16007E58A7 /* Extensions */, 470722FB2CBC15790046469F /* API */, 4763FFB72CB9134D00990336 /* Store.swift */, 4752A27C2CB96EB00073B784 /* CancleBag.swift */, 470722F92CBC0A870046469F /* Constants.swift */, - 470723172CBC29510046469F /* AuthenticationService.swift */, ); path = Util; sourceTree = ""; @@ -364,6 +381,7 @@ 4763FFAE2CB90AE000990336 /* AppState.swift */, 4763FFB02CB90C1500990336 /* DependencyInjector.swift */, 4763FFB22CB90E3800990336 /* InteractorsContainer.swift */, + 47F4F69A2CC8E24300543D24 /* ServicesContainer.swift */, ); path = Injected; sourceTree = ""; @@ -413,6 +431,7 @@ 47A147392CA138CB00A91F66 /* Core */ = { isa = PBXGroup; children = ( + 47F635032CC3EBF10034EAA9 /* Model */, 4763FFBB2CB9228B00990336 /* AppEnviroment.swift */, ); path = Core; @@ -421,9 +440,9 @@ 47A1473A2CA13DD000A91F66 /* DTO */ = { isa = PBXGroup; children = ( + 47F635002CC3E9700034EAA9 /* User */, + 47F634FC2CC3E8500034EAA9 /* Auth */, 470723122CBC1BF10046469F /* BaseResponse.swift */, - 4707230E2CBC1A9C0046469F /* LoginRequestDTO.swift */, - 470723102CBC1B0A0046469F /* LoginResponseDTO.swift */, ); path = DTO; sourceTree = ""; @@ -501,7 +520,7 @@ isa = PBXGroup; children = ( 47A1477F2CA15A4E00A91F66 /* OnboardingView.swift */, - 478933782CADBDA500E1D89E /* SignInView.swift */, + 478933782CADBDA500E1D89E /* SignUpView.swift */, ); path = Auth; sourceTree = ""; @@ -585,6 +604,7 @@ isa = PBXGroup; children = ( 47DF041E2CBE7A29007E58A7 /* Encodable+.swift */, + 47BE30E42CC81A9E0015D973 /* URLRequest+.swift */, ); path = Extensions; sourceTree = ""; @@ -599,6 +619,42 @@ path = Base; sourceTree = ""; }; + 47F4F69E2CC8E45A00543D24 /* Services */ = { + isa = PBXGroup; + children = ( + 470723172CBC29510046469F /* AuthenticationService.swift */, + 47BE30E22CC813BB0015D973 /* KeyChainManager.swift */, + ); + path = Services; + sourceTree = ""; + }; + 47F634FC2CC3E8500034EAA9 /* Auth */ = { + isa = PBXGroup; + children = ( + 4707230E2CBC1A9C0046469F /* LoginRequestDTO.swift */, + 470723102CBC1B0A0046469F /* LoginResponseDTO.swift */, + 47F4F6962CC88FBB00543D24 /* SignUpRequestDTO.swift */, + 47F4F6982CC89A6900543D24 /* SignUpResponseDTO.swift */, + ); + path = Auth; + sourceTree = ""; + }; + 47F635002CC3E9700034EAA9 /* User */ = { + isa = PBXGroup; + children = ( + 47F634FD2CC3E8A40034EAA9 /* NicknameCheckDTO.swift */, + ); + path = User; + sourceTree = ""; + }; + 47F635032CC3EBF10034EAA9 /* Model */ = { + isa = PBXGroup; + children = ( + 47F635042CC3EC1A0034EAA9 /* Department.swift */, + ); + path = Model; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -788,7 +844,9 @@ 471940D12CAFE61A00426D30 /* LineHeight.swift in Sources */, 4763FFBC2CB9228B00990336 /* AppEnviroment.swift in Sources */, 473671A12CB1234800527896 /* TodayWiseSaying.swift in Sources */, + 47F4F6992CC89A6900543D24 /* SignUpResponseDTO.swift in Sources */, 470722FE2CBC15DA0046469F /* NetworkProtocol.swift in Sources */, + 47F635052CC3EC1A0034EAA9 /* Department.swift in Sources */, 4795DC902CB43B2700EC3AB1 /* TimePickerDialog.swift in Sources */, 4752A27D2CB96EB00073B784 /* CancleBag.swift in Sources */, 4736719F2CB120A600527896 /* WeeklyStudy.swift in Sources */, @@ -797,20 +855,26 @@ 47860E092CB82BEF005701C0 /* TimePickerViewModel.swift in Sources */, 470723052CBC180D0046469F /* NetworkError.swift in Sources */, 475B86E52CA1CA61000534B2 /* HY2Button.swift in Sources */, + 47BE30E52CC81A9E0015D973 /* URLRequest+.swift in Sources */, 473671A52CB13D8100527896 /* SafeArea.swift in Sources */, 471940C82CAFCB1B00426D30 /* SizeCheckModifier.swift in Sources */, + 47F634FE2CC3E8A40034EAA9 /* NicknameCheckDTO.swift in Sources */, 4763FFAF2CB90AE000990336 /* AppState.swift in Sources */, 47B1D4AC2C9CB1740071B62B /* HongikYeolgong2App.swift in Sources */, 4763FFBA2CB913FF00990336 /* UserDataInteractor.swift in Sources */, 475B86E72CA1CC20000534B2 /* HY2TextField.swift in Sources */, - 478933792CADBDA500E1D89E /* SignInView.swift in Sources */, + 47F635022CC3E98D0034EAA9 /* UserEndpoint.swift in Sources */, + 478933792CADBDA500E1D89E /* SignUpView.swift in Sources */, 4763FFB52CB90EBD00990336 /* InitialView.swift in Sources */, 470723092CBC198E0046469F /* AuthRepository.swift in Sources */, + 47F4F6972CC88FBB00543D24 /* SignUpRequestDTO.swift in Sources */, 47A1474E2CA144EC00A91F66 /* StudyRoomUsage+Mock.swift in Sources */, 4763FFB32CB90E3800990336 /* InteractorsContainer.swift in Sources */, 470723182CBC29510046469F /* AuthenticationService.swift in Sources */, + 47BE30E32CC813BB0015D973 /* KeyChainManager.swift in Sources */, 470723132CBC1BF10046469F /* BaseResponse.swift in Sources */, 471940CD2CAFDAD700426D30 /* WeeklyRanking+Mock.swift in Sources */, + 47F4F69B2CC8E24300543D24 /* ServicesContainer.swift in Sources */, 47CA17252CC9336100CBB251 /* StudyPeriodView.swift in Sources */, 4763FFB82CB9134D00990336 /* Store.swift in Sources */, 47A147802CA15A4E00A91F66 /* OnboardingView.swift in Sources */, @@ -979,6 +1043,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = HongikYeolgong2/HongikYeolgong2.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; @@ -998,6 +1063,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen.storyboard"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Dark; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1023,6 +1089,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = HongikYeolgong2/HongikYeolgong2.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; @@ -1042,6 +1109,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen.storyboard"; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Dark; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/HongikYeolgong2/Core/AppEnviroment.swift b/HongikYeolgong2/Core/AppEnviroment.swift index 734597e..f19160d 100644 --- a/HongikYeolgong2/Core/AppEnviroment.swift +++ b/HongikYeolgong2/Core/AppEnviroment.swift @@ -15,8 +15,25 @@ extension AppEnviroment { static func bootstrap() -> AppEnviroment { let appState = Store(AppState()) let authRepository = AuthRepositoryImpl() - let interactors: DIContainer.Interactors = .init(userDataInteractor: UserDataInteractorImpl(appState: appState, authRepository: authRepository)) - let diContainer = DIContainer(appState: appState, interactors: interactors) + + let services: DIContainer.Services = .init( + authenticationService: AuthenticationServiceImpl() + ) + + let interactors: DIContainer.Interactors = .init( + userDataInteractor: UserDataInteractorImpl( + appState: appState, + authRepository: authRepository, + authService: services.authenticationService + ) + ) + + let diContainer = DIContainer( + appState: appState, + interactors: interactors, + services: services + ) + return .init(container: diContainer) } } diff --git a/HongikYeolgong2/Core/Model/Department.swift b/HongikYeolgong2/Core/Model/Department.swift new file mode 100644 index 0000000..d6a4b17 --- /dev/null +++ b/HongikYeolgong2/Core/Model/Department.swift @@ -0,0 +1,47 @@ +// +// Department.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/19/24. +// + +import Foundation + +enum Department: String, CaseIterable { + case constructionUrban = "건설도시공학부" + case civilEnvironmental = "건설환경공학과" + case architecture = "건축학부" + case business = "경영학부" + case economics = "경제학부" + case performingArts = "공연예술학부" + case metalDesign = "금속조형디자인과" + case mechanicalSystem = "기계시스템디자인공학과" + case koreanEdu = "국어교육과" + case koreanLit = "국어국문학과" + case urbanPlanning = "도시공학과" + case germanLit = "독어독문학과" + case orientalPainting = "동양화과" + case ceramics = "도예유리과" + case designManagement = "디자인경영융합학부" + case designArtManagement = "디자인·예술경영학부" + case design = "디자인학부" + case physicsEdu = "물리교육과" + case law = "법학부" + case frenchLit = "불어불문학과" + case socialEdu = "사회교육과" + case industrialDesign = "산업디자인학과" + case industrialData = "산업·데이터공학과" + case textileFashion = "섬유미술패션디자인과" + case mathEdu = "수학교육과" + case materials = "신소재화공시스템공학부" + case englishEdu = "영어교육과" + case englishLit = "영어영문학과" + case historyEdu = "역사교육과" + case artStudies = "예술학과" + case appliedArts = "응용미술학과" + case electricalElectronic = "전자전기공학부" + case sculpture = "조소과" + case computerScience = "컴퓨터공학과" + case frenchStudies = "프랑스어문학과" + case painting = "회화과" +} diff --git a/HongikYeolgong2/Data/DTO/LoginRequestDTO.swift b/HongikYeolgong2/Data/DTO/Auth/LoginRequestDTO.swift similarity index 88% rename from HongikYeolgong2/Data/DTO/LoginRequestDTO.swift rename to HongikYeolgong2/Data/DTO/Auth/LoginRequestDTO.swift index 36f479c..f38db6a 100644 --- a/HongikYeolgong2/Data/DTO/LoginRequestDTO.swift +++ b/HongikYeolgong2/Data/DTO/Auth/LoginRequestDTO.swift @@ -13,9 +13,6 @@ enum SocialLoginType: String, Encodable { } struct LoginRequestDTO: Encodable { - let socialPlatform: String + let email: String let idToken: String } - - - diff --git a/HongikYeolgong2/Data/DTO/LoginResponseDTO.swift b/HongikYeolgong2/Data/DTO/Auth/LoginResponseDTO.swift similarity index 100% rename from HongikYeolgong2/Data/DTO/LoginResponseDTO.swift rename to HongikYeolgong2/Data/DTO/Auth/LoginResponseDTO.swift diff --git a/HongikYeolgong2/Data/DTO/Auth/SignUpRequestDTO.swift b/HongikYeolgong2/Data/DTO/Auth/SignUpRequestDTO.swift new file mode 100644 index 0000000..151da5d --- /dev/null +++ b/HongikYeolgong2/Data/DTO/Auth/SignUpRequestDTO.swift @@ -0,0 +1,13 @@ +// +// SignUpRequestDTO.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/23/24. +// + +import Foundation + +struct SignUpRequestDTO: Encodable { + let nickname: String + let department: String +} diff --git a/HongikYeolgong2/Data/DTO/Auth/SignUpResponseDTO.swift b/HongikYeolgong2/Data/DTO/Auth/SignUpResponseDTO.swift new file mode 100644 index 0000000..5ce23fc --- /dev/null +++ b/HongikYeolgong2/Data/DTO/Auth/SignUpResponseDTO.swift @@ -0,0 +1,15 @@ +// +// SignUpResponseDTO.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/23/24. +// + +import Foundation + +struct SignUpResponseDTO: Decodable { + let id: Int + let username: String + let nickname: String + let department: String +} diff --git a/HongikYeolgong2/Data/DTO/BaseResponse.swift b/HongikYeolgong2/Data/DTO/BaseResponse.swift index a9973a9..032f5d4 100644 --- a/HongikYeolgong2/Data/DTO/BaseResponse.swift +++ b/HongikYeolgong2/Data/DTO/BaseResponse.swift @@ -7,6 +7,8 @@ import Foundation +/// 네트워크 기본응답 형식을 정의하는 제네릭 구조체 입니다. +/// 제네릭 타입은 Decodable 프로토콜을 준수 -> BaseResponse = try await NetworkManager.request() struct BaseResponse: Decodable { let code: Int let status: String diff --git a/HongikYeolgong2/Data/DTO/User/NicknameCheckDTO.swift b/HongikYeolgong2/Data/DTO/User/NicknameCheckDTO.swift new file mode 100644 index 0000000..de1ae09 --- /dev/null +++ b/HongikYeolgong2/Data/DTO/User/NicknameCheckDTO.swift @@ -0,0 +1,14 @@ +// +// NicknameCheckDTO.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/19/24. +// + +import Foundation + +/// 닉네임체크 응답 DTO +struct NicknameCheckDTO: Decodable { + let nickname: String + let duplicate: Bool +} diff --git a/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift b/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift index f87da5e..3e319b0 100644 --- a/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift +++ b/HongikYeolgong2/Data/Repositories/Auth/AuthRepositoryImpl.swift @@ -10,18 +10,68 @@ import Combine final class AuthRepositoryImpl: AuthRepository { - /// 소셜로그인 - func signIn(loginReqDto: LoginRequestDTO) -> AnyPublisher { - return Future { promise in + /// 소셜로그인 + /// - Parameter loginReqDto: 로그인 요청 DTO(이메일, identityToken) + /// - Returns: 로그인 응답 DTO(accessToken, 가입여부) + func signIn(loginReqDto: LoginRequestDTO) -> AnyPublisher { + return Future { promise in Task { do { - let response: BaseResponse = try await NetworkService.shared.request(endpoint: AuthEndpoint.login(loginReqDto)) - promise(.success(response.code == 200)) - } catch { + let response: BaseResponse = try await NetworkService.shared.request(endpoint: AuthEndpoint.login(loginReqDto: loginReqDto)) + promise(.success(response.data!)) + } catch let error as NetworkError { promise(.failure(error)) } } - }.eraseToAnyPublisher() } + + /// 닉네임 중복체크 + /// - Parameter nickname: 닉네임 + /// - Returns: 중복체크 여부 + func checkUserNickname(nickname: String) -> AnyPublisher { + return Future { promise in + Task { + do { + let response: BaseResponse = try await NetworkService.shared.request(endpoint: UserEndpoint.checkUserNickname(nickname: nickname)) + promise(.success(response.data?.duplicate ?? false)) + } catch let error as NetworkError { + promise(.failure(error)) + } + } + }.eraseToAnyPublisher() + } + + /// 회원가입을 요청합니다. + /// - Parameter signUpReqDto: 회원가입 요청 DTO(닉네임, 학과) + /// - Returns: 회원가입 응답 DTO(유저정보) + func signUp(signUpReqDto: SignUpRequestDTO) -> AnyPublisher { + return Future { promise in + Task { + do { + let response: BaseResponse = try await NetworkService.shared.request(endpoint: UserEndpoint.signUp(signUpReqDto: signUpReqDto)) + promise(.success(response.data!)) + } catch let error as NetworkError { + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() + } + + /// - Returns: 내정보 조회 + func getUser() -> AnyPublisher { + return Future { promise in + Task { + do { + let response: BaseResponse = try await NetworkService.shared.request(endpoint: UserEndpoint.getUser) + promise(.success(response.data!)) + } catch let error as NetworkError { + promise(.failure(error)) + + } + } + } + .eraseToAnyPublisher() + } } diff --git a/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift b/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift index 3e574d3..e126003 100644 --- a/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift +++ b/HongikYeolgong2/Domain/Interactors/UserDataInteractor.swift @@ -6,11 +6,16 @@ // import Foundation +import SwiftUI import Combine +import AuthenticationServices protocol UserDataInteractor: AnyObject { - func login(idToken: String) + func requestAppleLogin(_ authorization: ASAuthorization) + func signUp(nickname: String, department: Department) func logout() + func getUser() + func checkUserNickname(nickname: String, isValidate: Binding) } final class UserDataInteractorImpl: UserDataInteractor { @@ -18,24 +23,101 @@ final class UserDataInteractorImpl: UserDataInteractor { private let cancleBag = CancelBag() private let appState: Store private let authRepository: AuthRepository + private let authService: AuthenticationService - init(appState: Store, authRepository: AuthRepository) { + init(appState: Store, + authRepository: AuthRepository, + authService: AuthenticationService) { self.appState = appState self.authRepository = authRepository + self.authService = authService } - func login(idToken: String) { - - let loginReqDto: LoginRequestDTO = .init(socialPlatform: SocialLoginType.apple.rawValue, idToken: idToken) + /// 애플로그인을 요청합니다. + /// - Parameter authorization: ASAuthorization + func requestAppleLogin(_ authorization: ASAuthorization) { + guard let (email, idToken) = authService.requestAppleLogin(authorization) else { + return + } + let loginReqDto: LoginRequestDTO = .init(email: email, idToken: idToken) authRepository .signIn(loginReqDto: loginReqDto) - .sink(receiveCompletion: { _ in }, - receiveValue: { _ in }) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] loginResDto in + guard let self = self else { return } + + let isAlreadyExists = loginResDto.alreadyExist + + // 회원가입 여부에 따라서 화면분기 + if isAlreadyExists { + appState[\.appLaunchState] = .authenticated + } else { + appState[\.routing.onboarding.signUp] = true + } + + KeyChainManager.addItem(key: .accessToken, value: loginResDto.accessToken) + } + ) .store(in: cancleBag) } + /// 회원가입을 요청합니다. + /// - Parameters: + /// - nickname: 닉네임 + /// - department: 학과 + func signUp(nickname: String, department: Department) { + authRepository + .signUp(signUpReqDto: .init(nickname: nickname, department: department.rawValue)) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] _ in + guard let self = self else { return } + appState[\.appLaunchState] = .authenticated + } + ) + .store(in: cancleBag) + } + + /// 로그아웃 func logout() { appState[\.appLaunchState] = .notAuthenticated + KeyChainManager.deleteItem(key: .accessToken) + } + + /// 닉네임 중복체크 + /// - Parameters: + /// - nickname: 닉네임 + /// - isValidate: 중복여부 + func checkUserNickname(nickname: String, isValidate: Binding) { + authRepository + .checkUserNickname(nickname: nickname) + .replaceError(with: false) + .receive(on: DispatchQueue.main) + .sink { isValidate.wrappedValue = $0 } + .store(in: cancleBag) + } + + /// 로그인된 유저정보를 가져옵니다. + func getUser() { + authRepository + .getUser() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { [weak self] completion in + guard let self = self else { return } + switch completion { + case .finished: + appState[\.appLaunchState] = .authenticated + case let .failure(error): + appState[\.appLaunchState] = .notAuthenticated + } + }, + receiveValue: { _ in } + ) + .store(in: cancleBag) } } diff --git a/HongikYeolgong2/Domain/Interfaces/AuthRepository.swift b/HongikYeolgong2/Domain/Interfaces/AuthRepository.swift index 3e8eeb2..8a3fe31 100644 --- a/HongikYeolgong2/Domain/Interfaces/AuthRepository.swift +++ b/HongikYeolgong2/Domain/Interfaces/AuthRepository.swift @@ -8,5 +8,8 @@ import Combine protocol AuthRepository { - func signIn(loginReqDto: LoginRequestDTO) -> AnyPublisher + func signIn(loginReqDto: LoginRequestDTO) -> AnyPublisher + func signUp(signUpReqDto: SignUpRequestDTO) -> AnyPublisher + func checkUserNickname(nickname: String) -> AnyPublisher + func getUser() -> AnyPublisher } diff --git a/HongikYeolgong2/Injected/AppState.swift b/HongikYeolgong2/Injected/AppState.swift index cef4411..9000112 100644 --- a/HongikYeolgong2/Injected/AppState.swift +++ b/HongikYeolgong2/Injected/AppState.swift @@ -10,6 +10,7 @@ import SwiftUI final class AppState: ObservableObject { @Published var appLaunchState: AppLaunchState = .checkAuthentication @Published var userData = UserData() + @Published var routing = ViewRouting() } extension AppState { @@ -25,3 +26,9 @@ extension AppState { var isLoggedIn = false } } + +extension AppState { + struct ViewRouting: Equatable { + var onboarding = OnboardingView.Routing() + } +} diff --git a/HongikYeolgong2/Injected/DependencyInjector.swift b/HongikYeolgong2/Injected/DependencyInjector.swift index 13d389b..9d05786 100644 --- a/HongikYeolgong2/Injected/DependencyInjector.swift +++ b/HongikYeolgong2/Injected/DependencyInjector.swift @@ -12,16 +12,19 @@ import Combine struct DIContainer: EnvironmentKey { let appState: Store let interactors: Interactors + let services: Services - init(appState: Store, interactors: Interactors) { + init(appState: Store, interactors: Interactors, services: Services) { self.appState = appState self.interactors = interactors + self.services = services } static var defaultValue: Self { Self.default } - private static let `default` = Self(appState: .init(AppState()), - interactors: .default) + private static let `default` = Self(appState: .init(AppState()), + interactors: .default, + services: .default) } extension EnvironmentValues { @@ -32,11 +35,11 @@ extension EnvironmentValues { } extension View { -// func inject(_ appState: AppState) -> some View { -// let container = DIContainer(appState: .init(appState), -// interactors: .init(userDataInteractor: UserDataInteractorImpl(appState: .init(AppState())))) -// return inject(container) -// } + // func inject(_ appState: AppState) -> some View { + // let container = DIContainer(appState: .init(appState), + // interactors: .init(userDataInteractor: UserDataInteractorImpl(appState: .init(AppState())))) + // return inject(container) + // } func inject(_ container: DIContainer) -> some View { return self diff --git a/HongikYeolgong2/Injected/InteractorsContainer.swift b/HongikYeolgong2/Injected/InteractorsContainer.swift index 71c7339..76f9d4e 100644 --- a/HongikYeolgong2/Injected/InteractorsContainer.swift +++ b/HongikYeolgong2/Injected/InteractorsContainer.swift @@ -15,8 +15,12 @@ extension DIContainer { self.userDataInteractor = userDataInteractor } - static let `default` = Self(userDataInteractor: UserDataInteractorImpl(appState: Store(AppState()), - authRepository: AuthRepositoryImpl() - )) + static let `default` = Self( + userDataInteractor: UserDataInteractorImpl( + appState: Store(AppState()), + authRepository: AuthRepositoryImpl(), + authService: AuthenticationServiceImpl() + ) + ) } } diff --git a/HongikYeolgong2/Injected/ServicesContainer.swift b/HongikYeolgong2/Injected/ServicesContainer.swift new file mode 100644 index 0000000..a19abd8 --- /dev/null +++ b/HongikYeolgong2/Injected/ServicesContainer.swift @@ -0,0 +1,20 @@ +// +// ServicesContainer.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/23/24. +// + +import Foundation + +extension DIContainer { + struct Services { + let authenticationService: AuthenticationService + + init(authenticationService: AuthenticationService) { + self.authenticationService = authenticationService + } + + static let `default` = Self(authenticationService: AuthenticationServiceImpl()) + } +} diff --git a/HongikYeolgong2/Presentation/Auth/OnboardingView.swift b/HongikYeolgong2/Presentation/Auth/OnboardingView.swift index 0ebe9cc..5dabd5a 100644 --- a/HongikYeolgong2/Presentation/Auth/OnboardingView.swift +++ b/HongikYeolgong2/Presentation/Auth/OnboardingView.swift @@ -1,75 +1,99 @@ -// -// OnboardingView.swift -// HongikYeolgong2 -// -// Created by 권석기 on 9/23/24. -// - import SwiftUI +import Combine import AuthenticationServices struct OnboardingView: View { - @Environment(\.injected) var injected: DIContainer - - private let authService = AuthenticationService() + // MARK: - Properties + @Environment(\.injected) private var injected: DIContainer - @State private var seletedIndex = 0 - @State private var isActive = false + // MARK: - States + @State private var tabIndex = 0 @State private var routingState: Routing = .init() + private var routingBinding: Binding { + $routingState.dispatched(to: injected.appState, \.routing.onboarding) + } + // MARK: - Body var body: some View { NavigationView { - VStack { - Spacer() - - TabView(selection: $seletedIndex) { - Image(.onboarding01) - .tag(0) - Image(.onboarding02) - .tag(1) - Image(.onboarding03) - .tag(2) - } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) - - HStack(spacing: 16.adjustToScreenWidth) { - ForEach(0..<3, id: \.self) { index in - Group { - if index == seletedIndex { - Image(.shineCount02) - .frame(width: 9, height: 9) - } else { - Circle() - .frame(width: 9, height: 9) - .foregroundColor(.gray600) - } - } - } - } - - Button(action: { - - }, label: { - Image(.snsLogin) - .resizable() - .frame(maxWidth: .infinity, maxHeight: 52) - }) - .padding(.horizontal, 32.adjustToScreenWidth) - .padding(.top, 32.adjustToScreenHeight) - .padding(.bottom, 20.adjustToScreenHeight) - .overlay ( - SignInWithAppleButton(onRequest: onRequestAppleLogin, onCompletion: onCompleteAppleLogin) - .blendMode(.destinationOver) - ) - - NavigationLink("SignIn", destination: SignInView(), isActive: $isActive) - .opacity(0) - .frame(width: 0, height: 0) + content + .onReceive(routingUpdate) { self.routingState = $0 } + } + } + + // MARK: - Main Contents + private var content: some View { + VStack { + Spacer() + onboardingPageView + pageIndicator + appleLoginButton + hiddenNavigationLink + } + } + + // MARK: - UI Components + private var onboardingPageView: some View { + TabView(selection: $tabIndex) { + Image(.onboarding01) + .tag(0) + Image(.onboarding02) + .tag(1) + Image(.onboarding03) + .tag(2) + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + } + + private var pageIndicator: some View { + HStack(spacing: 16.adjustToScreenWidth) { + ForEach(0..<3, id: \.self) { index in + indicatorDot(for: index) + } + } + } + + private func indicatorDot(for index: Int) -> some View { + Group { + if index == tabIndex { + Image(.shineCount02) + .frame(width: 9, height: 9) + } else { + Circle() + .frame(width: 9, height: 9) + .foregroundColor(.gray600) } } } + + private var appleLoginButton: some View { + Button(action: {}) { + Image(.snsLogin) + .resizable() + .frame(maxWidth: .infinity, maxHeight: 52) + } + .padding(.horizontal, 32.adjustToScreenWidth) + .padding(.top, 32.adjustToScreenHeight) + .padding(.bottom, 20.adjustToScreenHeight) + .overlay( + SignInWithAppleButton( + onRequest: onRequestAppleLogin, + onCompletion: onCompleteAppleLogin + ) + .blendMode(.destinationOver) + ) + } + + private var hiddenNavigationLink: some View { + NavigationLink("", + destination: SignUpView(), + isActive: routingBinding.signUp) + .opacity(0) + .frame(width: 0, height: 0) + } } +// MARK: - Apple Login Methods private extension OnboardingView { func onRequestAppleLogin(_ request: ASAuthorizationAppleIDRequest) { request.requestedScopes = [.email] @@ -78,19 +102,23 @@ private extension OnboardingView { func onCompleteAppleLogin(_ result: Result) { switch result { case let .success(authorization): - guard let idToken = authService.requestAppleLogin(authorization) else { - return - } - injected.interactors.userDataInteractor.login(idToken: idToken) - case let .failure(error): + injected.interactors.userDataInteractor + .requestAppleLogin(authorization) + case .failure: break } } } +// MARK: - Routing private extension OnboardingView { - struct Routing: Equatable { - var signUp: String? + private var routingUpdate: AnyPublisher { + injected.appState.updates(for: \.routing.onboarding) } } +extension OnboardingView { + struct Routing: Equatable { + var signUp: Bool = false + } +} diff --git a/HongikYeolgong2/Presentation/Auth/SignInView.swift b/HongikYeolgong2/Presentation/Auth/SignInView.swift deleted file mode 100644 index df71def..0000000 --- a/HongikYeolgong2/Presentation/Auth/SignInView.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// SignInView.swift -// HongikYeolgong2 -// -// Created by 권석기 on 10/3/24. -// - -import SwiftUI - -struct SignInView: View { - @Environment(\.injected) var injected: DIContainer - @State private var isDisabled = true - @State private var selectedDepartment = "" - @State private var inputDepartment = "" - @State private var nickname = "" - - private let departments = [ - "건설도시공학부", - "건설환경공학과", - "건축학부", - "경영학부", - "경제학부", - "공연예술학부", - "금속조형디자인과", - "기계시스템디자인공학과", - "국어교육과", - "국어국문학과", - "도시공학과", - "독어독문학과", - "동양화과", - "도예유리과", - "디자인경영융합학부", - "디자인·예술경영학부", - "디자인학부", - "물리교육과", - "법학부", - "불어불문학과", - "사회교육과", - "산업디자인학과", - "산업·데이터공학과", - "섬유미술패션디자인과", - "수학교육과", - "신소재화공시스템공학부", - "영어교육과", - "영어영문학과", - "역사교육과", - "예술학과", - "응용미술학과", - "일본어문학과", - "전자전기공학부", - "조소과", - "컴퓨터공학과", - "판화과", - "프랑스어문학과", - "회화과" - ] - - var body: some View { - ZStack { - Color.dark.edgesIgnoringSafeArea(.all) - VStack(spacing: 0) { - titleView - - // 가입폼 - VStack(spacing: 0) { - // 닉네임 - nicknameField - - selecteDepartment - } - .padding(.top, 23) - - Spacer() - - HY2Button(title: "", - style: .imageButton(image: isDisabled ? .submitButtonDisable : .submitButtonEnable)) { - - } - .padding(.bottom, 20) - } - .padding(.horizontal, 32) - - } - .toolbar(.hidden, for: .navigationBar) - } - - private var titleView: some View { - Text("회원가입") - .font(.suite(size: 18, weight: .bold)) - .foregroundStyle(.gray100) - .frame(maxWidth: .infinity, minHeight: 52, alignment: .leading) - } - - private var nicknameField: some View { - VStack { - HStack { - Text("닉네임") - .font(.pretendard(size: 16, weight: .bold)) - .foregroundStyle(.gray200) - - Spacer() - } - - HStack { - HY2TextField(text: $nickname, - placeholder: "닉네임을 입력해주세요", - isError: false) - - HY2Button(title: "중복확인", style: .mediumButton, backgroundColor: .blue100) { - isDisabled = false - } - .frame(width: 88) - } - .padding(.top, 8) - - HStack { - Text("* 한글, 영어, 숫자를 포함하여 2~8자를 입력해 주세요.") - .font(.pretendard(size: 12, weight: .regular)) - .foregroundStyle(.gray400) - .padding(.top, 4) - Spacer() - } - } - } - - private var selecteDepartment: some View { - VStack(spacing: 0) { - HStack { - Text("학과") - .font(.pretendard(size: 16, weight: .bold)) - .foregroundStyle(.gray200) - - Spacer() - } - - DropDownPicker(text: $inputDepartment, - seletedItem: $selectedDepartment, - placeholder: "학과를 입력해주세요", - items: departments) - .padding(.top, 8) - } - .padding(.top, 12) - } -} diff --git a/HongikYeolgong2/Presentation/Auth/SignUpView.swift b/HongikYeolgong2/Presentation/Auth/SignUpView.swift new file mode 100644 index 0000000..c8d276d --- /dev/null +++ b/HongikYeolgong2/Presentation/Auth/SignUpView.swift @@ -0,0 +1,233 @@ +// +// SignInView.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/3/24. +// + +import SwiftUI +import Combine + +struct SignUpView: View { + @Environment(\.injected) var injected: DIContainer + @State private var signupData = SignupData() + @State private var isSubmitButtonAvailable = false + @State private var isCheckButtonAvailable = false + + var body: some View { + content + .onChange(of: signupData.nickname, perform: { validateUserNickname(nickname: $0)} ) + .onChange(of: signupData.nicknameStatus, perform: { isCheckButtonAvailable = $0 == .none }) + .toolbar(.hidden, for: .navigationBar) + } +} + +// MARK: - Main Content + +private extension SignUpView { + var content: some View { + ZStack { + Color.dark.edgesIgnoringSafeArea(.all) + VStack(spacing: 0) { + titleView + signupForm + Spacer() + submitButton + } + .padding(.horizontal, 32.adjustToScreenWidth) + } + } +} + +// MARK: - Subviews + +private extension SignUpView { + var titleView: some View { + Text("회원가입") + .font(.suite(size: 18, weight: .bold)) + .foregroundStyle(.gray100) + .frame(maxWidth: .infinity, minHeight: 52.adjustToScreenHeight, alignment: .leading) + } + + var signupForm: some View { + VStack(spacing: 0) { + nicknameField + selecteDepartment + } + .padding(.top, 23.adjustToScreenHeight) + } + + var nicknameField: some View { + VStack(alignment: .leading, spacing: 8) { + Text("닉네임") + .font(.pretendard(size: 16, weight: .bold)) + .foregroundStyle(.gray200) + + HStack { + HY2TextField(text: $signupData.nickname, + placeholder: "닉네임을 입력해주세요", + isError: signupData.nicknameStatus.isError) + + duplicateCheckButton + } + + Text(signupData.nicknameStatus.message) + .font(.pretendard(size: 12, weight: .regular)) + .foregroundStyle(signupData.nicknameStatus.textColor) + .padding(.top, 4.adjustToScreenHeight) + } + } + + var duplicateCheckButton: some View { + Button(action: requestCheckNickname) { + Text("중복확인") + .font(.pretendard(size: 16, weight: .regular)) + .frame(maxWidth: .infinity, maxHeight: 48.adjustToScreenHeight) + .foregroundColor(.white) + } + .frame(width: 88.adjustToScreenWidth) + .background(isCheckButtonAvailable ? .blue100 : .blue400) + .disabled(!isCheckButtonAvailable) + .cornerRadius(8) + } + + var selecteDepartment: some View { + VStack(alignment: .leading, spacing: 8) { + Text("학과") + .font(.pretendard(size: 16, weight: .bold)) + .foregroundStyle(.gray200) + + DropDownPicker(text: $signupData.inputDepartment, + seletedItem: Binding( + get: { signupData.department.rawValue }, + set: { signupData.department = .init(rawValue: $0) ?? .appliedArts } + ), + placeholder: "학과를 입력해주세요", + items: Department.allCases.map { $0.rawValue }) + } + .padding(.top, 12.adjustToScreenHeight) + } + + var submitButton: some View { + Button(action: performSignUp, label: { + Image(isSubmitButtonAvailable ? .submitButtonEnable : .submitButtonDisable) + .resizable() + .frame(height: 50.adjustToScreenHeight) + }) + .padding(.bottom, 20.adjustToScreenHeight) + } +} + +// MARK: - Helper Methods + +private extension SignUpView { + func requestCheckNickname() { + injected.interactors.userDataInteractor + .checkUserNickname(nickname: signupData.nickname, + isValidate: $signupData.isNicknameAvailable) + } + + func performSignUp() { + injected.interactors.userDataInteractor + .signUp(nickname: signupData.nickname, + department: signupData.department) + } + + func validateUserNickname(nickname: String) { + if (!nickname.isEmpty && nickname.count < 2) || (!nickname.isEmpty && nickname.count > 8) { + signupData.nicknameStatus = .notAllowedLength + } else if nickname.contains(" ") { + signupData.nicknameStatus = .specialCharactersAndSpaces + } else if checkSpecialCharacter(nickname) { + signupData.nicknameStatus = .specialCharactersAndSpaces + } else if checkKoreanLang(nickname) { + signupData.nicknameStatus = .none + } else { + signupData.nicknameStatus = .unknown + } + } + + func checkSpecialCharacter(_ input: String) -> Bool { + let pattern: String = "[!\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~€£¥₩¢₹©®™§¶°•※≡∞≠≈‽✓✔✕✖←→↑↓↔↕↩↪↖↗↘↙ñ¡¿éèêëçäöüßàìòùåøæ]" + + if let _ = input.range(of: pattern, options: .regularExpression) { + return true + } else { + return false + } + } + + func checkKoreanLang(_ input: String) -> Bool { + let pattern = "^[가-힣a-zA-Z\\s]*$" + + if let _ = input.range(of: pattern, options: .regularExpression) { + return true + } else { + return false + } + } +} + +// MARK: - SignupData + +private extension SignUpView { + struct SignupData { + var nickname = "" + var inputDepartment = "" + var department: Department = .appliedArts + var nicknameStatus: NicknameStatus = .none + var isNicknameAvailable = false + } +} + +// MARK: - Nickname Status + +private extension SignUpView { + enum NicknameStatus { + case none // 기본상태 + case specialCharactersAndSpaces // 특수문자, 공백 + case notAllowedLength // 글자수 오류 + case available // 사용가능 + case alreadyUse // 사용중인 닉네임 + case unknown // 그외 + + var message: String { + switch self { + case .none: + "*한글, 영어, 숫자를 포함하여 2~8자를 입력해 주세요." + case .specialCharactersAndSpaces: + "*특수문자와 띄어쓰기를 사용할 수 없어요." + case .notAllowedLength: + "*한글, 영어, 숫자를 포함하여 2~8자를 입력해 주세요." + case .available: + "*닉네임을 사용할 수 있어요." + case .alreadyUse: + "*이미 사용중인 닉네임 입니다." + case .unknown: + "*올바른 형식의 닉네임이 아닙니다." + } + } + + var textColor: Color { + switch self { + case .none: + .gray400 + case .available: + .blue100 + default: + .yellow300 + } + } + + var isError: Bool { + switch self { + case .none: + false + case .available: + false + default: + true + } + } + } +} diff --git a/HongikYeolgong2/Presentation/Root/InitialView.swift b/HongikYeolgong2/Presentation/Root/InitialView.swift index 408a816..4b957e3 100644 --- a/HongikYeolgong2/Presentation/Root/InitialView.swift +++ b/HongikYeolgong2/Presentation/Root/InitialView.swift @@ -15,9 +15,8 @@ struct InitialView: View { var body: some View { Group { -// content -// .onReceive(isAppLaunchStateUpdated) { appLaunchState = $0 } - MainTabView() + content + .onReceive(isAppLaunchStateUpdated) { appLaunchState = $0 } } } @@ -43,9 +42,7 @@ private extension InitialView { private extension InitialView { func appLaunchCompleted() { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - injected.interactors.userDataInteractor.logout() - } + injected.interactors.userDataInteractor.getUser() } } diff --git a/HongikYeolgong2/Util/API/Base/NetworkError.swift b/HongikYeolgong2/Util/API/Base/NetworkError.swift index 406e51d..06904e6 100644 --- a/HongikYeolgong2/Util/API/Base/NetworkError.swift +++ b/HongikYeolgong2/Util/API/Base/NetworkError.swift @@ -12,4 +12,17 @@ enum NetworkError: Error { case invalidResponse case decodingError(String) case serverError(statusCode: Int) + + var message: String { + switch self { + case .invalidUrl: + "올바르지 않은 URL 입니다." + case .invalidResponse: + "올바르지 않은 응답형식 입니다." + case .decodingError: + "디코딩 에러" + case let .serverError(statusCode): + "서버에러: \(statusCode)" + } + } } diff --git a/HongikYeolgong2/Util/API/Base/NetworkProtocol.swift b/HongikYeolgong2/Util/API/Base/NetworkProtocol.swift index 1451955..b0b3983 100644 --- a/HongikYeolgong2/Util/API/Base/NetworkProtocol.swift +++ b/HongikYeolgong2/Util/API/Base/NetworkProtocol.swift @@ -33,3 +33,5 @@ protocol EndpointProtocol { var headers: [String: String]? { get } var body: Data? { get } } + + diff --git a/HongikYeolgong2/Util/API/Base/NetworkService.swift b/HongikYeolgong2/Util/API/Base/NetworkService.swift index 3c72632..be70850 100644 --- a/HongikYeolgong2/Util/API/Base/NetworkService.swift +++ b/HongikYeolgong2/Util/API/Base/NetworkService.swift @@ -48,7 +48,7 @@ extension NetworkService { } private func configRequest(url: URL, endpoint: EndpointProtocol) -> URLRequest { - var request = URLRequest(url: url) + var request = URLRequest(url) request.httpMethod = endpoint.method.rawValue if let headers = endpoint.headers { diff --git a/HongikYeolgong2/Util/API/Endpoints/AuthEndpoint.swift b/HongikYeolgong2/Util/API/Endpoints/AuthEndpoint.swift index b943adc..c3df8bd 100644 --- a/HongikYeolgong2/Util/API/Endpoints/AuthEndpoint.swift +++ b/HongikYeolgong2/Util/API/Endpoints/AuthEndpoint.swift @@ -11,17 +11,17 @@ import Foundation enum AuthEndpoint: EndpointProtocol { /// 애플 소셜 로그인 - case login(LoginRequestDTO) + case login(loginReqDto: LoginRequestDTO) } extension AuthEndpoint { var baseURL: URL? { - URL(string: "\(baseUrl)/auth") + URL(string: "\(SecretKeys.baseUrl)/auth") } var path: String { switch self { case .login: - "/login" + "/login-apple" } } diff --git a/HongikYeolgong2/Util/API/Endpoints/UserEndpoint.swift b/HongikYeolgong2/Util/API/Endpoints/UserEndpoint.swift new file mode 100644 index 0000000..11c19cd --- /dev/null +++ b/HongikYeolgong2/Util/API/Endpoints/UserEndpoint.swift @@ -0,0 +1,72 @@ +// +// UserEndpoint.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/19/24. +// + +import Foundation + +/// 유저 관련 엔드포인트 정의 +enum UserEndpoint: EndpointProtocol { + + /// 애플 소셜 로그인 + case checkUserNickname(nickname: String) + + /// 회원가입 + case signUp(signUpReqDto: SignUpRequestDTO) + + /// 유저정보 + case getUser +} + +extension UserEndpoint { + var baseURL: URL? { + URL(string: "\(SecretKeys.baseUrl)/user") + } + var path: String { + switch self { + case .checkUserNickname: + "/duplicate-nickname" + case .signUp: + "/join" + case .getUser: + "/me" + } + } + + var method: NetworkMethod { + switch self { + case .checkUserNickname, .signUp: + .post + case .getUser: + .get + } + } + + var parameters: [URLQueryItem]? { + switch self { + case .checkUserNickname, .signUp, .getUser: + nil + } + } + + var headers: [String: String]? { + switch self { + default: + ["Content-Type": "application/json"] + } + } + + var body: Data? { + switch self { + case let .checkUserNickname(nickname): + let parameter: [String: Any] = ["nickname": nickname] + return parameter.toData() + case let .signUp(signUpReqDto): + return signUpReqDto.toData() + case .getUser: + return nil + } + } +} diff --git a/HongikYeolgong2/Util/AuthenticationService.swift b/HongikYeolgong2/Util/AuthenticationService.swift deleted file mode 100644 index cdedbbf..0000000 --- a/HongikYeolgong2/Util/AuthenticationService.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// AuthenticationService.swift -// HongikYeolgong2 -// -// Created by 권석기 on 10/14/24. -// - -import Foundation -import AuthenticationServices - -class AuthenticationService: NSObject, ASAuthorizationControllerDelegate { - func requestAppleLogin(_ authorization: ASAuthorization) -> String? { - guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, - let idTokenData = appleIDCredential.identityToken, - let idToken = String(data: idTokenData, encoding: .utf8) else { - return nil - } - - return idToken - } -} diff --git a/HongikYeolgong2/Util/Constants.swift b/HongikYeolgong2/Util/Constants.swift index 2723a48..f703091 100644 --- a/HongikYeolgong2/Util/Constants.swift +++ b/HongikYeolgong2/Util/Constants.swift @@ -7,4 +7,8 @@ import Foundation -let baseUrl = Bundle.main.infoDictionary?["BaseURL"] as? String ?? "" +struct SecretKeys { + static let baseUrl = Bundle.main.infoDictionary?["BaseURL"] as? String ?? "" +} + + diff --git a/HongikYeolgong2/Util/Extensions/Encodable+.swift b/HongikYeolgong2/Util/Extensions/Encodable+.swift index b5ff820..c6410da 100644 --- a/HongikYeolgong2/Util/Extensions/Encodable+.swift +++ b/HongikYeolgong2/Util/Extensions/Encodable+.swift @@ -19,3 +19,14 @@ extension Encodable { } } } + +extension Dictionary { + func toData() -> Data? { + do { + let data = try JSONSerialization.data(withJSONObject: self) + return data + } catch { + return nil + } + } +} diff --git a/HongikYeolgong2/Util/Extensions/URLRequest+.swift b/HongikYeolgong2/Util/Extensions/URLRequest+.swift new file mode 100644 index 0000000..866d545 --- /dev/null +++ b/HongikYeolgong2/Util/Extensions/URLRequest+.swift @@ -0,0 +1,17 @@ +// +// URLRequest+.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/23/24. +// + +import Foundation + +// TODO: 추후에 Interceptor를 지원하는 라이브러리 사용의논 +extension URLRequest { + init(_ url: URL) { + self.init(url: url) + let accessToken = KeyChainManager.readItem(key: .accessToken) ?? "" + self.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + } +} diff --git a/HongikYeolgong2/Util/Services/AuthenticationService.swift b/HongikYeolgong2/Util/Services/AuthenticationService.swift new file mode 100644 index 0000000..b1f0c03 --- /dev/null +++ b/HongikYeolgong2/Util/Services/AuthenticationService.swift @@ -0,0 +1,31 @@ +// +// AuthenticationService.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/14/24. +// + +import Foundation +import AuthenticationServices + +protocol AuthenticationService { + func requestAppleLogin(_ authrization: ASAuthorization) -> (email: String, idToken: String)? +} + +final class AuthenticationServiceImpl: AuthenticationService { + + /// 애플로그인 요청을 위한 이메일과 토큰을 반환합니다.(이메일은 첫로그인시 반환) + /// - Parameter authorization: authorization + /// - Returns: email, identityToken + func requestAppleLogin(_ authorization: ASAuthorization) -> (email: String, idToken: String)? { + guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, + let idTokenData = appleIDCredential.identityToken, + let idToken = String(data: idTokenData, encoding: .utf8) else { + return nil + } + + let email = appleIDCredential.email ?? "testtt" + + return (email: email, idToken: idToken) + } +} diff --git a/HongikYeolgong2/Util/Services/KeyChainManager.swift b/HongikYeolgong2/Util/Services/KeyChainManager.swift new file mode 100644 index 0000000..fc60ac1 --- /dev/null +++ b/HongikYeolgong2/Util/Services/KeyChainManager.swift @@ -0,0 +1,108 @@ +// +// KeyChainManager.swift +// HongikYeolgong2 +// +// Created by 권석기 on 10/23/24. +// + +import Foundation + +struct KeyChainManager { + + /// KeyChainName을 enum으로 정의 + enum KeyChainName: String { + case accessToken = "accessToken" + } + + static let service = Bundle.main.bundleIdentifier ?? "com.teamHY2.HongikYeolgong2" + + /// 키체인에 값을 추가합니다. + /// - Parameters: + /// - key: KeyChainName + /// - value: String + static func addItem(key: KeyChainName, value: String) { + + let valueData = value.data(using: .utf8)! + + let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: key.rawValue, + kSecValueData: valueData] + + let status = SecItemAdd(query as CFDictionary, nil) + + if status == errSecSuccess { + print("add success") + } else if status == errSecDuplicateItem { + updateItem(key: key, value: value) + } else { + print("add failed") + } + } + + /// key에 해당하는 키체인 값을 가져옵니다. + /// - Parameters: + /// - key: KeyChainName + static func readItem(key: KeyChainName) -> String? { + + let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: key.rawValue, + kSecReturnAttributes: true, + kSecReturnData: true] + + var item: CFTypeRef? + if SecItemCopyMatching(query as CFDictionary, &item) != errSecSuccess { + print("read failed") + return nil + } + + guard let existItem = item as? [String:Any], + let data = existItem[kSecValueData as String] as? Data, + let returnValue = String(data: data, encoding: .utf8) else { + return nil + } + + return returnValue + } + + /// key에 해당하는 키값을 업데이트 합니다. + /// - Parameters: + /// - key: KeyChainName + /// - value: String + static func updateItem(key: KeyChainName, value: String) { + + let valueData = value.data(using: .utf8)! + + let previousQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: key.rawValue] + + let updateQuery: [CFString: Any] = [kSecValueData: valueData] + + let status = SecItemUpdate(previousQuery as CFDictionary, updateQuery as CFDictionary) + + if status == errSecSuccess { + print("update complete") + } else { + print("not finished update") + } + } + + + /// Key에 해당하는 키체인값을 삭제합니다. + /// - Parameter key: KeyChainName + static func deleteItem(key: KeyChainName) { + + let deleteQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: key.rawValue] + + let status = SecItemDelete(deleteQuery as CFDictionary) + if status == errSecSuccess { + print("remove key-value data complete") + } else { + print("remove key-value data failed") + } + } +} diff --git a/HongikYeolgong2/Util/Store.swift b/HongikYeolgong2/Util/Store.swift index bc90e42..185d38d 100644 --- a/HongikYeolgong2/Util/Store.swift +++ b/HongikYeolgong2/Util/Store.swift @@ -34,3 +34,25 @@ extension Store { return map(keyPath).removeDuplicates().eraseToAnyPublisher() } } + +extension Binding where Value: Equatable { + func dispatched(to state: Store, + _ keyPath: WritableKeyPath) -> Self { + return onSet { state[keyPath] = $0 } + } +} + +extension Binding where Value: Equatable { + typealias ValueClosure = (Value) -> Void + + func onSet(_ perform: @escaping ValueClosure) -> Self { + return .init(get: { () -> Value in + self.wrappedValue + }, set: { value in + if self.wrappedValue != value { + self.wrappedValue = value + } + perform(value) + }) + } +}