-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat-T3-102] 온보딩 서버 통신 구현 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e319300
2c96ac7
73857e6
6435a2e
04f265d
b2ebbf1
5612f65
032a2ed
e15754e
85d4de0
c911a0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // | ||
| // EmptyResponseDTO.swift | ||
| // DataSource | ||
| // | ||
| // Created by 최정인 on 7/15/25. | ||
| // | ||
|
|
||
| struct EmptyResponseDTO: Decodable {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // | ||
| // RecommendedRoutineDTO.swift | ||
| // DataSource | ||
| // | ||
| // Created by 최정인 on 7/15/25. | ||
| // | ||
|
|
||
| import Domain | ||
|
|
||
| struct RecommendedRoutineListResponseDTO: Decodable { | ||
| let recommendedRoutines: [RecommendedRoutineDTO] | ||
| } | ||
|
|
||
| struct RecommendedRoutineDTO: Decodable { | ||
| let id: Int | ||
| let routineName: String | ||
| let routineDescription: String | ||
| let subRoutines: [SubRoutine] | ||
|
|
||
| enum CodingKeys: String, CodingKey { | ||
| case id = "recommendedRoutineId" | ||
| case routineName = "recommendedRoutineName" | ||
| case routineDescription | ||
| case subRoutines = "recommendedSubRoutines" | ||
| } | ||
|
|
||
| func toRecommendedRoutineEntity() -> RecommendedRoutineEntity { | ||
| return RecommendedRoutineEntity( | ||
| id: id, | ||
| title: routineName, | ||
| description: routineDescription) | ||
| } | ||
| } | ||
|
|
||
| struct SubRoutine: Decodable { | ||
| let id: Int | ||
| let routineName: String | ||
|
|
||
| enum CodingKeys: String, CodingKey { | ||
| case id = "recommendedSubRoutineId" | ||
| case routineName = "recommendedSubRoutineName" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // | ||
| // OnboardingEndpoint.swift | ||
| // DataSource | ||
| // | ||
| // Created by 최정인 on 7/15/25. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum OnboardingEndpoint { | ||
| case registerOnboarding(accessToken: String, choices: [String: String]) | ||
| case registerRecommendedRoutine(accessToken: String, selectedRoutines: [Int]) | ||
| } | ||
|
|
||
| extension OnboardingEndpoint: Endpoint { | ||
| var baseURL: String { | ||
| return AppProperties.baseURL + "/api/v1/onboardings" | ||
| } | ||
|
|
||
| var path: String { | ||
| switch self { | ||
| case .registerOnboarding: baseURL | ||
| case .registerRecommendedRoutine: baseURL + "/routines" | ||
| } | ||
| } | ||
|
|
||
| var method: HTTPMethod { | ||
| return .post | ||
| } | ||
|
|
||
| var headers: [String : String] { | ||
| var headers: [String: String] = [ | ||
| "Content-Type": "application/json", | ||
| "accept": "*/*" | ||
| ] | ||
|
|
||
| switch self { | ||
| case .registerOnboarding(let accessToken, _): | ||
| headers["Authorization"] = "Bearer \(accessToken)" | ||
| case .registerRecommendedRoutine(let accessToken, _): | ||
| headers["Authorization"] = "Bearer \(accessToken)" | ||
| } | ||
|
|
||
| return headers | ||
| } | ||
|
|
||
| var queryParameters: [String : String] { | ||
| return [:] | ||
| } | ||
|
|
||
| var bodyParameters: [String : Any] { | ||
| switch self { | ||
| case .registerOnboarding(_, let choices): | ||
| return choices | ||
| case .registerRecommendedRoutine(_, let selectedRoutines): | ||
| return ["recommendedRoutineIds": selectedRoutines] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,12 +11,12 @@ import Shared | |||||||||||||
| import KakaoSDKUser | ||||||||||||||
| import KakaoSDKAuth | ||||||||||||||
|
|
||||||||||||||
| public final class AuthRepository: AuthRepositoryProtocol { | ||||||||||||||
| final class AuthRepository: AuthRepositoryProtocol { | ||||||||||||||
| private let networkService: NetworkServiceProtocol | ||||||||||||||
| private let keychainStorage: KeychainStorageProtocol | ||||||||||||||
| private let userDefaultsStorage: UserDefaultsStorageProtocol | ||||||||||||||
|
|
||||||||||||||
| public init( | ||||||||||||||
| init( | ||||||||||||||
| networkService: NetworkServiceProtocol, | ||||||||||||||
| keychainStorage: KeychainStorageProtocol, | ||||||||||||||
| userDefaultsStorage: UserDefaultsStorageProtocol | ||||||||||||||
|
|
@@ -26,52 +26,60 @@ public final class AuthRepository: AuthRepositoryProtocol { | |||||||||||||
| self.userDefaultsStorage = userDefaultsStorage | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public func kakaoLogin() async throws { | ||||||||||||||
| func kakaoLogin() async throws -> UserEntity { | ||||||||||||||
| let accessToken = try await fetchKakaoToken() | ||||||||||||||
| try await requestServerLogin(socialType: .kakao, nickname: nil, token: accessToken) | ||||||||||||||
| let user = try await requestServerLogin( | ||||||||||||||
| socialType: .kakao, | ||||||||||||||
| nickname: nil, | ||||||||||||||
| token: accessToken) | ||||||||||||||
| return user | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public func appleLogin(nickname: String?, authToken: String) async throws { | ||||||||||||||
| func appleLogin(nickname: String?, authToken: String) async throws -> UserEntity { | ||||||||||||||
| var savedNickname: String = "" | ||||||||||||||
| if let nickname { | ||||||||||||||
| try saveNickname(nickname: nickname) | ||||||||||||||
| savedNickname = nickname | ||||||||||||||
| } else { | ||||||||||||||
| savedNickname = try loadNickname() | ||||||||||||||
| } | ||||||||||||||
| try await requestServerLogin(socialType: .apple, nickname: savedNickname, token: authToken) | ||||||||||||||
| let user = try await requestServerLogin( | ||||||||||||||
| socialType: .apple, | ||||||||||||||
| nickname: savedNickname, | ||||||||||||||
| token: authToken) | ||||||||||||||
| return user | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public func submitAgreement(agreements: [TermsType : Bool]) async throws { | ||||||||||||||
| func submitAgreement(agreements: [TermsType : Bool]) async throws { | ||||||||||||||
| let accessToken = try loadToken(tokenType: .accessToken) | ||||||||||||||
| let endpoint = AuthEndpoint.agreements(accessToken: accessToken, agreements: agreements) | ||||||||||||||
| let response = try await networkService.request(endpoint: endpoint, type: BaseResponseDTO<EmptyResponse>.self) | ||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "\(response)") | ||||||||||||||
| _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public func logout() async throws { | ||||||||||||||
| func logout() async throws { | ||||||||||||||
| let accessToken = try loadToken(tokenType: .accessToken) | ||||||||||||||
| let endpoint = AuthEndpoint.logout(accessToken: accessToken) | ||||||||||||||
| let response = try await networkService.request(endpoint: endpoint, type: BaseResponseDTO<String>.self) | ||||||||||||||
| _ = try await networkService.request(endpoint: endpoint, type: String.self) | ||||||||||||||
| try removeToken() | ||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "\(response.message)") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public func withdraw() async throws { | ||||||||||||||
| func withdraw() async throws { | ||||||||||||||
| let accessToken = try loadToken(tokenType: .accessToken) | ||||||||||||||
| let endpoint = AuthEndpoint.withdraw(accessToken: accessToken) | ||||||||||||||
| let response = try await networkService.request(endpoint: endpoint, type: BaseResponseDTO<String>.self) | ||||||||||||||
| _ = try await networkService.request(endpoint: endpoint, type: String.self) | ||||||||||||||
| try removeToken() | ||||||||||||||
| try removeNickname() | ||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "\(response.message)") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public func reissueToken() async throws { | ||||||||||||||
| func reissueToken() async throws { | ||||||||||||||
| let refreshToken = try loadToken(tokenType: .refreshToken) | ||||||||||||||
| let endpoint = AuthEndpoint.reissue(refreshToken: refreshToken) | ||||||||||||||
| let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self) | ||||||||||||||
|
|
||||||||||||||
| guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self) | ||||||||||||||
| else { return } | ||||||||||||||
| let userEntity = userResponse.toUserEntity() | ||||||||||||||
|
Comment on lines
+78
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nil 응답 시 에러를 던져야 함
다음과 같이 수정하세요: - guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
- else { return }
+ guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
+ else { throw AuthError.invalidUserData }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| guard | ||||||||||||||
| let userEntity = userResponse.data?.toUserEntity(), | ||||||||||||||
| saveToken(tokenType: .accessToken, token: userEntity.accessToken), | ||||||||||||||
| saveToken(tokenType: .refreshToken, token: userEntity.refreshToken) | ||||||||||||||
| else { throw AuthError.tokenSaveFailed } | ||||||||||||||
|
|
@@ -80,7 +88,7 @@ public final class AuthRepository: AuthRepositoryProtocol { | |||||||||||||
| BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(userEntity.accessToken)") | ||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(userEntity.refreshToken)") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private func fetchKakaoToken() async throws -> String { | ||||||||||||||
| try await withCheckedThrowingContinuation { continuation in | ||||||||||||||
| let resultHandler: (OAuthToken?, Error?) -> Void = { oauthToken, error in | ||||||||||||||
|
|
@@ -107,22 +115,26 @@ public final class AuthRepository: AuthRepositoryProtocol { | |||||||||||||
| socialType: SocialLoginType, | ||||||||||||||
| nickname: String?, | ||||||||||||||
| token: String | ||||||||||||||
| ) async throws { | ||||||||||||||
| ) async throws -> UserEntity { | ||||||||||||||
| let endpoint = AuthEndpoint.login( | ||||||||||||||
| socialLoginType: socialType, | ||||||||||||||
| nickname: nickname, | ||||||||||||||
| token: token) | ||||||||||||||
|
|
||||||||||||||
| let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self) | ||||||||||||||
| guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self) | ||||||||||||||
| else { throw AuthError.invalidUserData } | ||||||||||||||
|
|
||||||||||||||
| let userEntity = userResponse.toUserEntity() | ||||||||||||||
| guard | ||||||||||||||
| let userEntity = userResponse.data?.toUserEntity(), | ||||||||||||||
| saveToken(tokenType: .accessToken, token: userEntity.accessToken), | ||||||||||||||
| saveToken(tokenType: .refreshToken, token: userEntity.refreshToken) | ||||||||||||||
| else { throw AuthError.tokenSaveFailed } | ||||||||||||||
|
|
||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "User Logined: \(userEntity.userState)") | ||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(userEntity.accessToken)") | ||||||||||||||
| BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(userEntity.refreshToken)") | ||||||||||||||
|
|
||||||||||||||
| return userEntity | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private func saveToken(tokenType: TokenType, token: String) -> Bool { | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // | ||
| // OnboardingRepository.swift | ||
| // DataSource | ||
| // | ||
| // Created by 최정인 on 7/15/25. | ||
| // | ||
|
|
||
| import Domain | ||
|
|
||
| final class OnboardingRepository: OnboardingRepositoryProtocol { | ||
| private let networkService: NetworkServiceProtocol | ||
| private let keychainStorage: KeychainStorageProtocol | ||
|
|
||
| init(networkService: NetworkServiceProtocol, keychainStorage: KeychainStorageProtocol) { | ||
| self.networkService = networkService | ||
| self.keychainStorage = keychainStorage | ||
| } | ||
|
|
||
| func registerOnboarding(onboardingChoices: [String : String]) async throws -> [RecommendedRoutineEntity] { | ||
| let accessToken = try loadToken(tokenType: .accessToken) | ||
| let endpoint = OnboardingEndpoint.registerOnboarding(accessToken: accessToken, choices: onboardingChoices) | ||
|
|
||
| guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) | ||
| else { return [] } | ||
|
|
||
| let recommendedRoutineEntity = response.recommendedRoutines.compactMap({ $0.toRecommendedRoutineEntity() }) | ||
| return recommendedRoutineEntity | ||
| } | ||
|
|
||
| func registerRecommendedRoutines(selectedRoutines: [Int]) async throws { | ||
| let accessToken = try loadToken(tokenType: .accessToken) | ||
| let endpoint = OnboardingEndpoint.registerRecommendedRoutine(accessToken: accessToken, selectedRoutines: selectedRoutines) | ||
| _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) | ||
| } | ||
|
|
||
| private func loadToken(tokenType: TokenType) throws -> String { | ||
| guard let token = keychainStorage.load(forKey: tokenType.rawValue) else { | ||
| throw AuthError.tokenLoadFailed | ||
| } | ||
| return token | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
프로토콜 계약 변경에 대한 검증 필요
네트워크 서비스 프로토콜의 반환 타입이
T에서T?로 변경되었습니다. 이는 올바른 변경이지만, 기존 코드에서 nil 처리가 적절히 되어 있는지 확인이 필요합니다.다음 스크립트를 실행하여 NetworkServiceProtocol 사용처에서 nil 처리가 적절히 되어 있는지 확인해주세요:
🏁 Script executed:
Length of output: 7493
네트워크 서비스 호출의 Optional 반환값을 명시적으로 처리하세요
NetworkServiceProtocol의request반환 타입이T?로 변경되면서, 일부 호출부에서 nil 케이스를 처리하지 않고 있습니다. 아래 사용처를 확인하여guard let또는if let/??등을 통해 Optional 바인딩을 추가해주세요.• Projects/DataSource/Sources/Repository/OnboardingRepository.swift
–
registerRecommendedRoutines(selectedRoutines:)내```swift
• Projects/DataSource/Sources/Repository/AuthRepository.swift
–
logout()/withdraw()내```swift
기존에 Optional 바인딩을 사용하고 있는
OnboardingRepository.fetchRecommendedRoutines()나reissueToken()과 같은 사례를 참고하여, 모든request(…, type:)호출에 대해 nil 반환을 명시적으로 처리해야 합니다.🤖 Prompt for AI Agents