Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol {
let networkService = DIContainer.shared.resolve(type: NetworkServiceProtocol.self),
let keychainStorage = DIContainer.shared.resolve(type: KeychainStorageProtocol.self),
let userDefaultsStorage = DIContainer.shared.resolve(type: UserDefaultsStorageProtocol.self)
else { return }
else { fatalError("networkService, keychainStorage, userDefaultsStorage 의존성이 등록되지 않았습니다.") }

DIContainer.shared.register(type: AuthRepositoryProtocol.self) { _ in
return AuthRepository(
networkService: networkService,
keychainStorage: keychainStorage,
userDefaultsStorage: userDefaultsStorage)
}

DIContainer.shared.register(type: OnboardingRepositoryProtocol.self) { _ in
return OnboardingRepository(networkService: networkService, keychainStorage: keychainStorage)
}
}
}
3 changes: 3 additions & 0 deletions Projects/DataSource/Sources/Common/Error/AuthError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum AuthError: Error, CustomStringConvertible {
case nicknameSaveFailed
case nicknameLoadFailed
case nicknameRemoveFailed
case invalidUserData
case unknown(Error)

public var description: String {
Expand All @@ -31,6 +32,8 @@ enum AuthError: Error, CustomStringConvertible {
return "닉네임 불러오기에 실패했습니다."
case .nicknameRemoveFailed:
return "닉네임 삭제에 실패했습니다."
case .invalidUserData:
return "서버 응답에 사용자 정보가 포함되어 있지 않습니다."
case .unknown(let error):
return "알 수 없는 에러가 발생했습니다. \(error.localizedDescription)"
}
Expand Down
8 changes: 8 additions & 0 deletions Projects/DataSource/Sources/DTO/EmptyResponseDTO.swift
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 {}
6 changes: 2 additions & 4 deletions Projects/DataSource/Sources/DTO/LoginResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

import Domain

typealias LoginResponseDTO = BaseResponseDTO<LoginResponse>

struct LoginResponse: Decodable {
struct LoginResponseDTO: Decodable {
let accessToken: String
let refreshToken: String
let userState: String
Expand All @@ -21,7 +19,7 @@ struct LoginResponse: Decodable {
}
}

extension LoginResponse {
extension LoginResponseDTO {
func toUserEntity() -> UserEntity {
return UserEntity(
accessToken: accessToken,
Expand Down
43 changes: 43 additions & 0 deletions Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift
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"
}
}
59 changes: 59 additions & 0 deletions Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift
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
Expand Up @@ -15,5 +15,5 @@ public protocol NetworkServiceProtocol {
/// - endpoint: 요청을 보낼 API Endpoint 정보
/// - type: 디코딩할 Response DTO 타입
/// - Returns: 응답 데이터를 디코딩한 객체
func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T
func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T?

Copy link
Copy Markdown

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:

#!/bin/bash
# NetworkServiceProtocol의 request 메서드 사용처를 찾아서 nil 처리 확인
echo "=== NetworkServiceProtocol.request 메서드 사용처 검색 ==="
rg -A 10 "\.request\(" --type swift

echo "=== 옵셔널 바인딩 또는 nil 체크 패턴 확인 ==="
rg -A 5 -B 5 "\.request\(" --type swift | rg -A 5 -B 5 "(if let|guard let|??|\?\.)"

Length of output: 7493


네트워크 서비스 호출의 Optional 반환값을 명시적으로 처리하세요

NetworkServiceProtocolrequest 반환 타입이 T?로 변경되면서, 일부 호출부에서 nil 케이스를 처리하지 않고 있습니다. 아래 사용처를 확인하여 guard let 또는 if let/?? 등을 통해 Optional 바인딩을 추가해주세요.

• Projects/DataSource/Sources/Repository/OnboardingRepository.swift
registerRecommendedRoutines(selectedRoutines:)
```swift

  • _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self)
  • guard let _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) else {
  •   // nil 처리: 에러 던지기 또는 기본값 반환
    
  •   return
    
  • }
    
    

• Projects/DataSource/Sources/Repository/AuthRepository.swift
logout()/withdraw()
```swift

  • _ = try await networkService.request(endpoint: endpoint, type: String.self)
  • guard let _ = try await networkService.request(endpoint: endpoint, type: String.self) else {
  •   // nil 처리: 에러 던지기 또는 로직 분기
    
  •   return
    
  • }
    
    

기존에 Optional 바인딩을 사용하고 있는 OnboardingRepository.fetchRecommendedRoutines()reissueToken()과 같은 사례를 참고하여, 모든 request(…, type:) 호출에 대해 nil 반환을 명시적으로 처리해야 합니다.

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift at line 18,
the request function now returns an optional T?, but some call sites do not
handle the nil case. Review all calls to request(endpoint:type:) in
OnboardingRepository.swift and AuthRepository.swift, and add explicit optional
binding using guard let or if let to handle nil results properly. Follow
existing examples like fetchRecommendedRoutines() and reissueToken() to ensure
that when request returns nil, the code either throws an error, returns early,
or provides a default value as appropriate.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

nil 응답 시 에러를 던져야 함

reissueToken() 메서드에서 서버 응답이 nil일 때 조용히 return하는 것은 문제가 될 수 있습니다. 토큰 재발급 실패는 중요한 에러 상황이므로 명시적으로 처리해야 합니다.

다음과 같이 수정하세요:

-        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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { return }
let userEntity = userResponse.toUserEntity()
guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self)
else { throw AuthError.invalidUserData }
let userEntity = userResponse.toUserEntity()
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/AuthRepository.swift around lines 72
to 74, the code silently returns when the server response is nil in the
reissueToken() method. Instead, modify the guard statement to throw a specific
error when userResponse is nil, ensuring that token reissue failures are
explicitly handled and propagated as errors.


guard
let userEntity = userResponse.data?.toUserEntity(),
saveToken(tokenType: .accessToken, token: userEntity.accessToken),
saveToken(tokenType: .refreshToken, token: userEntity.refreshToken)
else { throw AuthError.tokenSaveFailed }
Expand All @@ -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
Expand All @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions Projects/DataSource/Sources/Repository/OnboardingRepository.swift
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
}
}
12 changes: 9 additions & 3 deletions Projects/Domain/Sources/DomainDependencyAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ public struct DomainDependencyAssembler: DependencyAssemblerProtocol {
public func assemble() {
preAssembler.assemble()

guard let authRepository = DIContainer.shared.resolve(type: AuthRepositoryProtocol.self) else {
return
}
guard let authRepository = DIContainer.shared.resolve(type: AuthRepositoryProtocol.self)
else { fatalError("authRepository 의존성이 등록되지 않았습니다.") }

DIContainer.shared.register(type: LoginUseCaseProtocol.self) { _ in
return LoginUseCase(authRepository: authRepository)
Expand All @@ -33,5 +32,12 @@ public struct DomainDependencyAssembler: DependencyAssemblerProtocol {
DIContainer.shared.register(type: WithdrawUseCaseProtocol.self) { _ in
return WithdrawUseCase(authRepository: authRepository)
}

DIContainer.shared.register(type: OnboardingUseCaseProtocol.self) { container in
guard let onboardingRepository = container.resolve(type: OnboardingRepositoryProtocol.self)
else { fatalError("onboardingRepository 의존성이 등록되지 않았습니다.") }

return OnboardingUseCase(onboardingRepository: onboardingRepository)
}
}
}
Loading
Loading