From d7f66c048185d5c9d0fff306e45ae334faee4c49 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 17:02:22 +0900 Subject: [PATCH 01/14] =?UTF-8?q?del:=20#3=20-=20Networks=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EA=B8=B0=EC=A1=B4=20empty=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/Networks/Sources/API/APIEmpty.swift | 8 -------- Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift | 8 -------- .../Modules/Networks/Sources/Entity/EntityEmpty.swift | 8 -------- .../Modules/Networks/Sources/Service/ServiceEmpty.swift | 8 -------- 4 files changed, 32 deletions(-) delete mode 100644 Projects/Modules/Networks/Sources/API/APIEmpty.swift delete mode 100644 Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift delete mode 100644 Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift delete mode 100644 Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift diff --git a/Projects/Modules/Networks/Sources/API/APIEmpty.swift b/Projects/Modules/Networks/Sources/API/APIEmpty.swift deleted file mode 100644 index cc161f1..0000000 --- a/Projects/Modules/Networks/Sources/API/APIEmpty.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// APIEmpty.swift -// 27th-App-Team-1-iOSManifests -// -// Created by 최안용 on 1/13/26. -// - -import Foundation diff --git a/Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift b/Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift deleted file mode 100644 index c0b8f52..0000000 --- a/Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// NBaseEmpty.swift -// 27th-App-Team-1-iOSManifests -// -// Created by 최안용 on 1/13/26. -// - -import Foundation diff --git a/Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift b/Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift deleted file mode 100644 index be4230d..0000000 --- a/Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// EntityEmpty.swift -// 27th-App-Team-1-iOSManifests -// -// Created by 최안용 on 1/13/26. -// - -import Foundation diff --git a/Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift b/Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift deleted file mode 100644 index a45ef69..0000000 --- a/Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// ServiceEmpty.swift -// 27th-App-Team-1-iOSManifests -// -// Created by 최안용 on 1/13/26. -// - -import Foundation From 52a0cb5fce76368d27d4db8dc3df04bf70547c28 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 17:07:24 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=20feat:=20#3-=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20Base=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Base/APIErrorProtocol.swift | 15 +++++++++++ .../Networks/Sources/Base/BaseResponse.swift | 15 +++++++++++ .../Networks/Sources/Base/ErrorResponse.swift | 20 ++++++++++++++ .../Networks/Sources/Base/NetworkError.swift | 26 +++++++++++++++++++ .../Networks/Sources/Base/NetworkResult.swift | 15 +++++++++++ 5 files changed, 91 insertions(+) create mode 100644 Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift create mode 100644 Projects/Modules/Networks/Sources/Base/BaseResponse.swift create mode 100644 Projects/Modules/Networks/Sources/Base/ErrorResponse.swift create mode 100644 Projects/Modules/Networks/Sources/Base/NetworkError.swift create mode 100644 Projects/Modules/Networks/Sources/Base/NetworkResult.swift diff --git a/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift b/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift new file mode 100644 index 0000000..9c8dbc8 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift @@ -0,0 +1,15 @@ +// +// APIErrorProtocol.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +protocol APIErrorProtocol: Error { + var message: String { get } + + init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) +} diff --git a/Projects/Modules/Networks/Sources/Base/BaseResponse.swift b/Projects/Modules/Networks/Sources/Base/BaseResponse.swift new file mode 100644 index 0000000..38672b0 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Base/BaseResponse.swift @@ -0,0 +1,15 @@ +// +// BaseResponse.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +struct BaseResponse: Decodable { + let code: String + let message: String + let data: T? +} diff --git a/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift b/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift new file mode 100644 index 0000000..64f157a --- /dev/null +++ b/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift @@ -0,0 +1,20 @@ +// +// ErrorResponse.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +struct ErrorResponse: Decodable { + let code: String? + let message: String? + let errors: [ErrorDetail]? + + struct ErrorDetail: Decodable { + let field: String? + let message: String? + } +} diff --git a/Projects/Modules/Networks/Sources/Base/NetworkError.swift b/Projects/Modules/Networks/Sources/Base/NetworkError.swift new file mode 100644 index 0000000..a1c2496 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Base/NetworkError.swift @@ -0,0 +1,26 @@ +// +// NetworkError.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum NetworkError: Error { + case connectionFailed + case decodingFailed + case unknown(Error) + + var message: String { + switch self { + case .connectionFailed: + return "네트워크 연결을 확인해주세요" + case .decodingFailed: + return "데이터 처리 중 오류가 발생했습니다" + case .unknown: + return "알 수 없는 오류가 발생했습니다" + } + } +} diff --git a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift new file mode 100644 index 0000000..71f6d86 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift @@ -0,0 +1,15 @@ +// +// NetworkResult.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum NetworkResult { + case success(T) + case failure(E) + case networkFailure(NetworkError) +} From a428a406b8b3e71695688bf21038ad78597e9f5c Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 17:16:34 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20#3=20-=20Moya=20async/await=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Networks/Sources/Base/EmptyResponse.swift | 11 ++ .../Extensions/MoyaProvider+Async.swift | 108 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 Projects/Modules/Networks/Sources/Base/EmptyResponse.swift create mode 100644 Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift diff --git a/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift b/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift new file mode 100644 index 0000000..3a52474 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift @@ -0,0 +1,11 @@ +// +// EmptyResponse.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +struct EmptyResponse: Decodable {} diff --git a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift new file mode 100644 index 0000000..826795a --- /dev/null +++ b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift @@ -0,0 +1,108 @@ +// +// MoyaProvider+Async.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation +import Moya + +extension MoyaProvider { + private static var successCode: String { "2000" } + + func request( + _ target: Target, + errorType: E.Type + ) async -> NetworkResult { + await withCheckedContinuation { continuation in + self.request(target) { result in + switch result { + case .success(let response): + do { + let baseResponse = try JSONDecoder().decode( + BaseResponse.self, + from: response.data + ) + + if baseResponse.code == Self.successCode { + guard let data = baseResponse.data else { + continuation.resume(returning: .networkFailure(.decodingFailed)) + return + } + continuation.resume(returning: .success(data)) + } else { + let errorResponse = try JSONDecoder().decode( + ErrorResponse.self, + from: response.data + ) + let apiError = E( + code: errorResponse.code ?? "", + message: errorResponse.message ?? "", + errors: errorResponse.errors ?? [] + ) + continuation.resume(returning: .failure(apiError)) + } + } catch { + continuation.resume(returning: .networkFailure(.decodingFailed)) + } + + case .failure(let moyaError): + let networkError = Self.mapMoyaError(moyaError) + continuation.resume(returning: .networkFailure(networkError)) + } + } + } + } + + func requestPlain( + _ target: Target, + errorType: E.Type + ) async -> NetworkResult { + await withCheckedContinuation { continuation in + self.request(target) { result in + switch result { + case .success(let response): + do { + let baseResponse = try JSONDecoder().decode( + BaseResponse.self, + from: response.data + ) + + if baseResponse.code == Self.successCode { + continuation.resume(returning: .success(())) + } else { + let errorResponse = try JSONDecoder().decode( + ErrorResponse.self, + from: response.data + ) + let apiError = E( + code: errorResponse.code ?? "", + message: errorResponse.message ?? "", + errors: errorResponse.errors ?? [] + ) + continuation.resume(returning: .failure(apiError)) + } + } catch { + continuation.resume(returning: .networkFailure(.decodingFailed)) + } + + case .failure(let moyaError): + let networkError = Self.mapMoyaError(moyaError) + continuation.resume(returning: .networkFailure(networkError)) + } + } + } + } + + private static func mapMoyaError(_ error: MoyaError) -> NetworkError { + switch error { + case .underlying(let nsError as NSError, _) + where nsError.domain == NSURLErrorDomain: + return .connectionFailed + default: + return .unknown(error) + } + } +} From cdebdd0d38755b77d588f679a9edba02d234a061 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 17:31:48 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20#3=20-=20Networks=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20BASE=5FURL=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 10 ++++++---- .../Foundation/NetworkConfiguration.swift | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift index 73a7f17..7661742 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -31,9 +31,10 @@ public extension Project { "NSAppTransportSecurity": .dictionary([ "NSAllowsArbitraryLoads": .boolean(true) ]), - "ITSAppUsesNonExemptEncryption": .boolean(false) + "ITSAppUsesNonExemptEncryption": .boolean(false), + "BASE_URL": .string("$(BASE_URL)") ] - + static let demoInfoPlist: [String: Plist.Value] = [ "CFBundleShortVersionString": .string("1.0.0"), "CFBundleDevelopmentRegion": .string("ko"), @@ -57,9 +58,10 @@ public extension Project { "NSAppTransportSecurity": .dictionary([ "NSAllowsArbitraryLoads": .boolean(true) ]), - "ITSAppUsesNonExemptEncryption": .boolean(false) + "ITSAppUsesNonExemptEncryption": .boolean(false), + "BASE_URL": .string("$(BASE_URL)") ] - + static let framework: InfoPlist = .extendingDefault(with: [ "CFBundlePackageType": "FMWK" ]) diff --git a/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift b/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift new file mode 100644 index 0000000..99d7c5c --- /dev/null +++ b/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift @@ -0,0 +1,19 @@ +// +// NetworkConfiguration.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +enum NetworkConfiguration { + static var baseURL: URL { + guard let urlString = Bundle.main.infoDictionary?["BASE_URL"] as? String, + let url = URL(string: urlString) else { + fatalError("BASE_URL not found in Info.plist") + } + return url + } +} From dbb4e1d528ae03705eaffc110feaf0958dc01708 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 18:04:51 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20#3=20-=20Signup=20API=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Error/Auth/SignupError.swift | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift diff --git a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift new file mode 100644 index 0000000..65fc429 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift @@ -0,0 +1,54 @@ +// +// SignupError.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + + +enum SignupField { + case fcmToken + case unknown(String) + + init(rawValue: String) { + switch rawValue { + case "fcmToken": self = .fcmToken + default: self = .unknown(rawValue) + } + } +} + +// MARK: - Error +struct SignupError: APIErrorProtocol { + let type: ErrorType + let message: String + let field: SignupField? + let fieldMessage: String? + + enum ErrorType { + case validationError + case internalServerError + case undefined(code: String) + } + + init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) { + self.message = message + + if let firstError = errors.first { + self.field = SignupField(rawValue: firstError.field ?? "") + self.fieldMessage = firstError.message + } else { + self.field = nil + self.fieldMessage = nil + } + + switch code { + case "COMM-01-005": self.type = .validationError + case "COMM-08-001": self.type = .internalServerError + default: self.type = .undefined(code: code) + } + } +} From 0fccc133ee468c45dad143d151f36ea669ce6c9b Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 18:28:12 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20#3-=20SigninDTO,=20AuthService,?= =?UTF-8?q?=20api=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Networks/Sources/DTO/Auth/SigninDTO.swift | 23 ++++++++++ .../Sources/Service/AuthService.swift | 27 +++++++++++ .../Networks/Sources/TargetType/AuthAPI.swift | 45 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 Projects/Modules/Networks/Sources/DTO/Auth/SigninDTO.swift create mode 100644 Projects/Modules/Networks/Sources/Service/AuthService.swift create mode 100644 Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift diff --git a/Projects/Modules/Networks/Sources/DTO/Auth/SigninDTO.swift b/Projects/Modules/Networks/Sources/DTO/Auth/SigninDTO.swift new file mode 100644 index 0000000..2830bfe --- /dev/null +++ b/Projects/Modules/Networks/Sources/DTO/Auth/SigninDTO.swift @@ -0,0 +1,23 @@ +// +// Signin.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +struct SignupRequest: Encodable { + let fcmToken: String + let deviceModel: String? + let deviceOs: String? + let deviceOsVersion: String? + let appVersion: String? +} + +struct SignupResponse: Decodable { + let uuid: String + let accessToken: String + let nickname: String +} diff --git a/Projects/Modules/Networks/Sources/Service/AuthService.swift b/Projects/Modules/Networks/Sources/Service/AuthService.swift new file mode 100644 index 0000000..ee95e70 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Service/AuthService.swift @@ -0,0 +1,27 @@ +// +// AuthService.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation +import Moya + +protocol AuthServiceProtocol { + func signup(request: SignupRequest) async -> NetworkResult +} + + +final class AuthService: AuthServiceProtocol { + private let provider: MoyaProvider + + init(provider: MoyaProvider = MoyaProvider()) { + self.provider = provider + } + + func signup(request: SignupRequest) async -> NetworkResult { + await provider.request(.signup(request: request), errorType: SignupError.self) + } +} diff --git a/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift b/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift new file mode 100644 index 0000000..7a2d9fc --- /dev/null +++ b/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift @@ -0,0 +1,45 @@ +// +// AuthAPI.swift +// Networks +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation +import Moya + +enum AuthAPI { + case signup(request: SignupRequest) +} + +extension AuthAPI: TargetType { + var baseURL: URL { + return NetworkConfiguration.baseURL + } + + var path: String { + switch self { + case .signup: + return "/api/v1/auth/signup" + } + } + + var method: Moya.Method { + switch self { + case .signup: + return .post + } + } + + var task: Moya.Task { + switch self { + case .signup(let request): + return .requestJSONEncodable(request) + } + } + + var headers: [String: String]? { + return ["Content-Type": "application/json"] + } +} From 1c19b5bcdbdacd73d06e04e137490a66910f139c Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 18:29:32 +0900 Subject: [PATCH 07/14] =?UTF-8?q?chore:=20#3-=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=EC=88=98=EC=A0=95.=20signin=20->=20signup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DTO/Auth/{SigninDTO.swift => SignupDTO.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Projects/Modules/Networks/Sources/DTO/Auth/{SigninDTO.swift => SignupDTO.swift} (100%) diff --git a/Projects/Modules/Networks/Sources/DTO/Auth/SigninDTO.swift b/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift similarity index 100% rename from Projects/Modules/Networks/Sources/DTO/Auth/SigninDTO.swift rename to Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift From 1a0975c9a951afbea9ae589b83208cd216795d43 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Mon, 19 Jan 2026 19:44:52 +0900 Subject: [PATCH 08/14] =?UTF-8?q?chore:=20#3=20-=20internal=20->=20public?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20post=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=20api=20=ED=98=B8=EC=B6=9C=EB=90=A8=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/Networks/Project.swift | 19 +++++ .../Sources/Base/APIErrorProtocol.swift | 4 +- .../Networks/Sources/Base/ErrorResponse.swift | 16 ++-- .../Networks/Sources/Base/NetworkError.swift | 6 +- .../Networks/Sources/Base/NetworkResult.swift | 3 +- .../Networks/Sources/DTO/Auth/SignupDTO.swift | 34 +++++--- .../Sources/Error/Auth/SignupError.swift | 28 +++---- .../Foundation/NetworkConfiguration.swift | 13 +++- .../Sources/Service/AuthService.swift | 13 ++-- .../Networks/Sources/TargetType/AuthAPI.swift | 26 ++++--- .../Networks/Tests/AuthServiceTests.swift | 77 +++++++++++++++++++ 11 files changed, 178 insertions(+), 61 deletions(-) create mode 100644 Projects/Modules/Networks/Tests/AuthServiceTests.swift diff --git a/Projects/Modules/Networks/Project.swift b/Projects/Modules/Networks/Project.swift index 2414eb4..0d7fe6a 100644 --- a/Projects/Modules/Networks/Project.swift +++ b/Projects/Modules/Networks/Project.swift @@ -8,6 +8,7 @@ import ProjectDescription import ProjectDescriptionHelpers import DependencyPlugin +import EnvPlugin let project = Project.makeModule( name: "Networks", @@ -20,6 +21,24 @@ let project = Project.makeModule( scripts: [.swiftLint], isStatic: true, hasResources: false + ), + .target( + name: "NetworksTests", + destinations: .iOS, + product: .unitTests, + bundleId: "org.yapp.NDGL.NetworksTests", + deploymentTargets: .iOS("17.0"), + infoPlist: .extendingDefault(with: [ + "BASE_URL": .string("$(BASE_URL)") + ]), + sources: ["Tests/**"], + dependencies: [ + .target(name: "Networks") + ], + settings: .settings(configurations: [ + .debug(name: "Debug", xcconfig: .relativeToRoot("xcconfigs/Debug.xcconfig")), + .release(name: "Release", xcconfig: .relativeToRoot("xcconfigs/Release.xcconfig")) + ]) ) ] ) diff --git a/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift b/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift index 9c8dbc8..03fd382 100644 --- a/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift +++ b/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift @@ -8,8 +8,8 @@ import Foundation -protocol APIErrorProtocol: Error { +public protocol APIErrorProtocol: Error { var message: String { get } - + init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) } diff --git a/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift b/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift index 64f157a..a62ca4d 100644 --- a/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift +++ b/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift @@ -8,13 +8,13 @@ import Foundation -struct ErrorResponse: Decodable { - let code: String? - let message: String? - let errors: [ErrorDetail]? - - struct ErrorDetail: Decodable { - let field: String? - let message: String? +public struct ErrorResponse: Decodable { + public let code: String? + public let message: String? + public let errors: [ErrorDetail]? + + public struct ErrorDetail: Decodable { + public let field: String? + public let message: String? } } diff --git a/Projects/Modules/Networks/Sources/Base/NetworkError.swift b/Projects/Modules/Networks/Sources/Base/NetworkError.swift index a1c2496..5494c50 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkError.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkError.swift @@ -8,12 +8,12 @@ import Foundation -enum NetworkError: Error { +public enum NetworkError: Error { case connectionFailed case decodingFailed case unknown(Error) - - var message: String { + + public var message: String { switch self { case .connectionFailed: return "네트워크 연결을 확인해주세요" diff --git a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift index 71f6d86..cba44df 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift @@ -8,7 +8,8 @@ import Foundation -enum NetworkResult { +@frozen +public enum NetworkResult { case success(T) case failure(E) case networkFailure(NetworkError) diff --git a/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift b/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift index 2830bfe..263730d 100644 --- a/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift +++ b/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift @@ -8,16 +8,30 @@ import Foundation -struct SignupRequest: Encodable { - let fcmToken: String - let deviceModel: String? - let deviceOs: String? - let deviceOsVersion: String? - let appVersion: String? +public struct SignupRequest: Encodable { + public let fcmToken: String + public let deviceModel: String? + public let deviceOs: String? + public let deviceOsVersion: String? + public let appVersion: String? + + public init( + fcmToken: String, + deviceModel: String? = nil, + deviceOs: String? = nil, + deviceOsVersion: String? = nil, + appVersion: String? = nil + ) { + self.fcmToken = fcmToken + self.deviceModel = deviceModel + self.deviceOs = deviceOs + self.deviceOsVersion = deviceOsVersion + self.appVersion = appVersion + } } -struct SignupResponse: Decodable { - let uuid: String - let accessToken: String - let nickname: String +public struct SignupResponse: Decodable { + public let uuid: String + public let accessToken: String + public let nickname: String } diff --git a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift index 65fc429..aae4ba5 100644 --- a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift +++ b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift @@ -9,11 +9,11 @@ import Foundation -enum SignupField { +public enum SignupField { case fcmToken case unknown(String) - - init(rawValue: String) { + + public init(rawValue: String) { switch rawValue { case "fcmToken": self = .fcmToken default: self = .unknown(rawValue) @@ -22,21 +22,21 @@ enum SignupField { } // MARK: - Error -struct SignupError: APIErrorProtocol { - let type: ErrorType - let message: String - let field: SignupField? - let fieldMessage: String? - - enum ErrorType { +public struct SignupError: APIErrorProtocol { + public let type: ErrorType + public let message: String + public let field: SignupField? + public let fieldMessage: String? + + public enum ErrorType { case validationError case internalServerError case undefined(code: String) } - - init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) { + + public init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) { self.message = message - + if let firstError = errors.first { self.field = SignupField(rawValue: firstError.field ?? "") self.fieldMessage = firstError.message @@ -44,7 +44,7 @@ struct SignupError: APIErrorProtocol { self.field = nil self.fieldMessage = nil } - + switch code { case "COMM-01-005": self.type = .validationError case "COMM-08-001": self.type = .internalServerError diff --git a/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift b/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift index 99d7c5c..5b7d197 100644 --- a/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift +++ b/Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift @@ -8,12 +8,17 @@ import Foundation -enum NetworkConfiguration { - static var baseURL: URL { - guard let urlString = Bundle.main.infoDictionary?["BASE_URL"] as? String, +public enum NetworkConfiguration { + public static var baseURL: URL { + // 테스트 환경에서는 Bundle.main이 Xcode 도구를 가리키므로 테스트 번들에서 찾음 + let bundle = Bundle.allBundles.first { $0.bundlePath.hasSuffix(".xctest") } ?? Bundle.main + + guard let urlString = bundle.infoDictionary?["BASE_URL"] as? String, let url = URL(string: urlString) else { - fatalError("BASE_URL not found in Info.plist") + fatalError("BASE_URL not found in Info.plist") } return url } } + + diff --git a/Projects/Modules/Networks/Sources/Service/AuthService.swift b/Projects/Modules/Networks/Sources/Service/AuthService.swift index ee95e70..847cd1c 100644 --- a/Projects/Modules/Networks/Sources/Service/AuthService.swift +++ b/Projects/Modules/Networks/Sources/Service/AuthService.swift @@ -9,19 +9,18 @@ import Foundation import Moya -protocol AuthServiceProtocol { +public protocol AuthServiceProtocol { func signup(request: SignupRequest) async -> NetworkResult } - -final class AuthService: AuthServiceProtocol { +public final class AuthService: AuthServiceProtocol { private let provider: MoyaProvider - - init(provider: MoyaProvider = MoyaProvider()) { + + public init(provider: MoyaProvider = MoyaProvider()) { self.provider = provider } - - func signup(request: SignupRequest) async -> NetworkResult { + + public func signup(request: SignupRequest) async -> NetworkResult { await provider.request(.signup(request: request), errorType: SignupError.self) } } diff --git a/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift b/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift index 7a2d9fc..6589767 100644 --- a/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift +++ b/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift @@ -6,40 +6,42 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // +import Alamofire import Foundation import Moya -enum AuthAPI { +public enum AuthAPI { case signup(request: SignupRequest) } - + extension AuthAPI: TargetType { - var baseURL: URL { + public var baseURL: URL { return NetworkConfiguration.baseURL } - - var path: String { + + public var path: String { switch self { case .signup: - return "/api/v1/auth/signup" + return "/api/v1/auth/users" } } - - var method: Moya.Method { + + public var method: Moya.Method { switch self { case .signup: return .post } } - - var task: Moya.Task { + + public var task: Moya.Task { switch self { case .signup(let request): return .requestJSONEncodable(request) } } - - var headers: [String: String]? { + + public var headers: [String: String]? { return ["Content-Type": "application/json"] } + } diff --git a/Projects/Modules/Networks/Tests/AuthServiceTests.swift b/Projects/Modules/Networks/Tests/AuthServiceTests.swift new file mode 100644 index 0000000..894a540 --- /dev/null +++ b/Projects/Modules/Networks/Tests/AuthServiceTests.swift @@ -0,0 +1,77 @@ +// +// AuthServiceTests.swift +// NetworksTests +// +// Created by kimnahun on 1/19/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import XCTest +@testable import Networks + +final class AuthServiceTests: XCTestCase { + + var sut: AuthService! + + override func setUp() { + super.setUp() + sut = AuthService() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - Signup 실제 API 테스트 + func test_signup_실제API호출() async throws { + // Given + print("========== 테스트 시작 ==========") + let request = SignupRequest( + fcmToken: "test-fcm-token-12345", + deviceModel: "iPhone", + deviceOs: "iOS", + deviceOsVersion: "17.0", + appVersion: "1.0.0" + ) + print("Request 생성 완료") + + // When + print("API 호출 시작...") + let result = await sut.signup(request: request) + print("API 호출 완료") + + // Then + switch result { + case .success(let response): + print("========== 성공 ==========") + print("UUID: \(response.uuid)") + print("AccessToken: \(response.accessToken)") + print("Nickname: \(response.nickname)") + print("==============================") + XCTAssertFalse(response.uuid.isEmpty, "UUID가 비어있음") + XCTAssertFalse(response.accessToken.isEmpty, "AccessToken이 비어있음") + + case .failure(let error): + print("========== API 에러 ==========") + print("Type: \(error.type)") + print("Message: \(error.message)") + if let field = error.field { + print("Field: \(field)") + } + if let fieldMessage = error.fieldMessage { + print("FieldMessage: \(fieldMessage)") + } + print("=================================") + XCTAssertFalse(error.message.isEmpty, "에러 메시지가 비어있음") + + case .networkFailure(let error): + print("==========네트워크 에러 ==========") + print("Error: \(error.message)") + print("=====================================") + XCTFail("네트워크 연결 실패: \(error.message)") + } + + print("========== 테스트 종료 ==========") + } +} From cd3952ef8f3673ea103158ac3c9fc016604e1bd2 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Tue, 20 Jan 2026 09:45:47 +0900 Subject: [PATCH 09/14] =?UTF-8?q?chore:=20#3=20-=20BUILD=20LIBRARY=20FOR?= =?UTF-8?q?=20DISTRIBUTION=20=EC=84=A4=EC=A0=95=20off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이걸 켜두면 경고창이 뜨고 있어서 껐습니다. 참고 ) https://stackoverflow.com/questions/60162207/module-was-not-compiled-with-library-evolution-support-using-it-means-binary-co --- .../ProjectDescriptionHelpers/Settings+Extension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift index 1af74fa..180bb54 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift @@ -12,7 +12,7 @@ public extension Settings { static let frameworkSettings: Settings = .settings( base: [ "SKIP_INSTALL": "YES", - "BUILD_LIBRARY_FOR_DISTRIBUTION": "YES", + "BUILD_LIBRARY_FOR_DISTRIBUTION": "NO", "DEFINES_MODULE": "YES", "ENABLE_BITCODE": "NO", "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget), From 149bb1aa8cbc165addbbda543d42fdd506c25e1b Mon Sep 17 00:00:00 2001 From: kimnahun Date: Tue, 20 Jan 2026 09:49:12 +0900 Subject: [PATCH 10/14] =?UTF-8?q?chore:=20#3=20-=20Sendable=20=EC=B1=84?= =?UTF-8?q?=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/Networks/Sources/Base/BaseResponse.swift | 2 +- .../Modules/Networks/Sources/Base/EmptyResponse.swift | 2 +- .../Modules/Networks/Sources/Base/ErrorResponse.swift | 4 ++-- Projects/Modules/Networks/Sources/Base/NetworkError.swift | 8 ++++---- .../Modules/Networks/Sources/Base/NetworkResult.swift | 2 +- .../Modules/Networks/Sources/DTO/Auth/SignupDTO.swift | 4 ++-- .../Modules/Networks/Sources/Error/Auth/SignupError.swift | 6 +++--- .../Networks/Sources/Extensions/MoyaProvider+Async.swift | 6 +++--- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Projects/Modules/Networks/Sources/Base/BaseResponse.swift b/Projects/Modules/Networks/Sources/Base/BaseResponse.swift index 38672b0..0f398d6 100644 --- a/Projects/Modules/Networks/Sources/Base/BaseResponse.swift +++ b/Projects/Modules/Networks/Sources/Base/BaseResponse.swift @@ -8,7 +8,7 @@ import Foundation -struct BaseResponse: Decodable { +struct BaseResponse: Decodable, Sendable { let code: String let message: String let data: T? diff --git a/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift b/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift index 3a52474..841084e 100644 --- a/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift +++ b/Projects/Modules/Networks/Sources/Base/EmptyResponse.swift @@ -8,4 +8,4 @@ import Foundation -struct EmptyResponse: Decodable {} +struct EmptyResponse: Decodable, Sendable {} diff --git a/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift b/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift index a62ca4d..f154a75 100644 --- a/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift +++ b/Projects/Modules/Networks/Sources/Base/ErrorResponse.swift @@ -8,12 +8,12 @@ import Foundation -public struct ErrorResponse: Decodable { +public struct ErrorResponse: Decodable, Sendable { public let code: String? public let message: String? public let errors: [ErrorDetail]? - public struct ErrorDetail: Decodable { + public struct ErrorDetail: Decodable, Sendable { public let field: String? public let message: String? } diff --git a/Projects/Modules/Networks/Sources/Base/NetworkError.swift b/Projects/Modules/Networks/Sources/Base/NetworkError.swift index 5494c50..f1c1bb3 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkError.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkError.swift @@ -8,10 +8,10 @@ import Foundation -public enum NetworkError: Error { +public enum NetworkError: Error, Sendable { case connectionFailed case decodingFailed - case unknown(Error) + case unknown(String) public var message: String { switch self { @@ -19,8 +19,8 @@ public enum NetworkError: Error { return "네트워크 연결을 확인해주세요" case .decodingFailed: return "데이터 처리 중 오류가 발생했습니다" - case .unknown: - return "알 수 없는 오류가 발생했습니다" + case .unknown(let description): + return "알 수 없는 오류: \(description)" } } } diff --git a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift index cba44df..efbf298 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift @@ -9,7 +9,7 @@ import Foundation @frozen -public enum NetworkResult { +public enum NetworkResult: Sendable { case success(T) case failure(E) case networkFailure(NetworkError) diff --git a/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift b/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift index 263730d..1389403 100644 --- a/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift +++ b/Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift @@ -8,7 +8,7 @@ import Foundation -public struct SignupRequest: Encodable { +public struct SignupRequest: Encodable, Sendable { public let fcmToken: String public let deviceModel: String? public let deviceOs: String? @@ -30,7 +30,7 @@ public struct SignupRequest: Encodable { } } -public struct SignupResponse: Decodable { +public struct SignupResponse: Decodable, Sendable { public let uuid: String public let accessToken: String public let nickname: String diff --git a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift index aae4ba5..f20a9c1 100644 --- a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift +++ b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift @@ -9,7 +9,7 @@ import Foundation -public enum SignupField { +public enum SignupField: Sendable { case fcmToken case unknown(String) @@ -22,13 +22,13 @@ public enum SignupField { } // MARK: - Error -public struct SignupError: APIErrorProtocol { +public struct SignupError: APIErrorProtocol, Sendable { public let type: ErrorType public let message: String public let field: SignupField? public let fieldMessage: String? - public enum ErrorType { + public enum ErrorType: Sendable { case validationError case internalServerError case undefined(code: String) diff --git a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift index 826795a..fad933d 100644 --- a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift +++ b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift @@ -12,7 +12,7 @@ import Moya extension MoyaProvider { private static var successCode: String { "2000" } - func request( + func request( _ target: Target, errorType: E.Type ) async -> NetworkResult { @@ -56,7 +56,7 @@ extension MoyaProvider { } } - func requestPlain( + func requestPlain( _ target: Target, errorType: E.Type ) async -> NetworkResult { @@ -102,7 +102,7 @@ extension MoyaProvider { where nsError.domain == NSURLErrorDomain: return .connectionFailed default: - return .unknown(error) + return .unknown(error.localizedDescription) } } } From 47c9724d21fd74ddb86155aaf84341372a23bfb4 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Tue, 20 Jan 2026 10:00:58 +0900 Subject: [PATCH 11/14] =?UTF-8?q?del:=20#3-=20=EA=B0=9C=ED=96=89=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift index f20a9c1..01aa2b1 100644 --- a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift +++ b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift @@ -8,7 +8,6 @@ import Foundation - public enum SignupField: Sendable { case fcmToken case unknown(String) From 1737620a49d635ce144cdf275c12ab6b2eff1310 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Wed, 21 Jan 2026 01:14:55 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20#3:=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20data,=20domain,=20feature=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EA=B0=84=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20-=20=ED=81=B4=EB=A6=B0=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B3=80=EA=B2=BD.?= =?UTF-8?q?=20domain=EC=9D=B4=20data=EC=9D=98=EC=A1=B4=20X?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DI/AuthRepositoryFactory.swift | 19 +++++++ .../Repository/Auth/AuthRepository.swift | 40 ++++++++++++++ .../Transform/Auth/AuthTransform.swift | 27 ++++++++++ .../Auth/AuthRepositoryProtocol.swift | 13 +++++ .../Sources/Model/Auth/SignupError.swift | 27 ++++++++++ .../Sources/Model/Auth/SignupInfo.swift | 17 ++++++ .../Sources/Model/Auth/SignupResult.swift | 21 ++++++++ Projects/Modules/Networks/Project.swift | 3 +- .../Sources/Base/APIErrorProtocol.swift | 15 ------ .../Networks/Sources/Base/NetworkResult.swift | 4 +- .../Sources/Error/Auth/SignupError.swift | 53 ------------------- .../ErrorMapping/SignupError+Mapping.swift | 48 +++++++++++++++++ .../Extensions/MoyaProvider+Async.swift | 38 ++++++------- .../Sources/Service/AuthService.swift | 7 +-- 14 files changed, 239 insertions(+), 93 deletions(-) create mode 100644 Projects/Data/Sources/DI/AuthRepositoryFactory.swift create mode 100644 Projects/Data/Sources/Repository/Auth/AuthRepository.swift create mode 100644 Projects/Data/Sources/Transform/Auth/AuthTransform.swift create mode 100644 Projects/Domain/Sources/Interface/Auth/AuthRepositoryProtocol.swift create mode 100644 Projects/Domain/Sources/Model/Auth/SignupError.swift create mode 100644 Projects/Domain/Sources/Model/Auth/SignupInfo.swift create mode 100644 Projects/Domain/Sources/Model/Auth/SignupResult.swift delete mode 100644 Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift delete mode 100644 Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift create mode 100644 Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift diff --git a/Projects/Data/Sources/DI/AuthRepositoryFactory.swift b/Projects/Data/Sources/DI/AuthRepositoryFactory.swift new file mode 100644 index 0000000..257ca89 --- /dev/null +++ b/Projects/Data/Sources/DI/AuthRepositoryFactory.swift @@ -0,0 +1,19 @@ +// +// AuthRepositoryFactory.swift +// Data +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain +import Foundation +import Networks + +public func makeAuthService() -> AuthServiceProtocol { + AuthService() +} + +public func makeAuthRepository(authService: AuthServiceProtocol) -> AuthRepositoryProtocol { + AuthRepository(authService: authService) +} diff --git a/Projects/Data/Sources/Repository/Auth/AuthRepository.swift b/Projects/Data/Sources/Repository/Auth/AuthRepository.swift new file mode 100644 index 0000000..9f778dc --- /dev/null +++ b/Projects/Data/Sources/Repository/Auth/AuthRepository.swift @@ -0,0 +1,40 @@ +// +// AuthRepository.swift +// Data +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain +import Foundation +import Networks + +public final class AuthRepository: AuthRepositoryProtocol, @unchecked Sendable { + private let authService: AuthServiceProtocol + + public init(authService: AuthServiceProtocol) { + self.authService = authService + } + + public func signup(info: SignupInfo) async -> Result { + let request = SignupRequest(fcmToken: info.fcmToken) + let result = await authService.signup(request: request) + + switch result { + case .success(let response): + let signupResult = SignupResult( + uuid: response.uuid, + accessToken: response.accessToken, + nickname: response.nickname + ) + return .success(signupResult) + + case .failure(let error): + return .failure(error) + + case .networkFailure(let networkError): + return .failure(.networkError(message: networkError.localizedDescription)) + } + } +} diff --git a/Projects/Data/Sources/Transform/Auth/AuthTransform.swift b/Projects/Data/Sources/Transform/Auth/AuthTransform.swift new file mode 100644 index 0000000..8d76bac --- /dev/null +++ b/Projects/Data/Sources/Transform/Auth/AuthTransform.swift @@ -0,0 +1,27 @@ +// +// AuthTransform.swift +// Data +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain +import Foundation +import Networks + +extension SignupResult { + init(from response: SignupResponse) { + self.init( + uuid: response.uuid, + accessToken: response.accessToken, + nickname: response.nickname + ) + } +} + +extension SignupRequest { + init(from info: SignupInfo) { + self.init(fcmToken: info.fcmToken) + } +} diff --git a/Projects/Domain/Sources/Interface/Auth/AuthRepositoryProtocol.swift b/Projects/Domain/Sources/Interface/Auth/AuthRepositoryProtocol.swift new file mode 100644 index 0000000..8e52a0f --- /dev/null +++ b/Projects/Domain/Sources/Interface/Auth/AuthRepositoryProtocol.swift @@ -0,0 +1,13 @@ +// +// AuthRepositoryProtocol.swift +// Domain +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public protocol AuthRepositoryProtocol: Sendable { + func signup(info: SignupInfo) async -> Result +} diff --git a/Projects/Domain/Sources/Model/Auth/SignupError.swift b/Projects/Domain/Sources/Model/Auth/SignupError.swift new file mode 100644 index 0000000..fe1f7a6 --- /dev/null +++ b/Projects/Domain/Sources/Model/Auth/SignupError.swift @@ -0,0 +1,27 @@ +// +// SignupError.swift +// Domain +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +// MARK: - SignupField + +public enum SignupField: Sendable, Equatable { + case fcmToken + case email + case unknown(String) +} + +// MARK: - SignupError + +public enum SignupError: Error, Sendable { + case validationFailed(field: SignupField, message: String) + case userAlreadyExists + case serverError(message: String?) + case networkError(message: String) + case unknown(code: String, message: String) +} diff --git a/Projects/Domain/Sources/Model/Auth/SignupInfo.swift b/Projects/Domain/Sources/Model/Auth/SignupInfo.swift new file mode 100644 index 0000000..67ae93b --- /dev/null +++ b/Projects/Domain/Sources/Model/Auth/SignupInfo.swift @@ -0,0 +1,17 @@ +// +// SignupInfo.swift +// Domain +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct SignupInfo: Sendable { + public let fcmToken: String + + public init(fcmToken: String) { + self.fcmToken = fcmToken + } +} diff --git a/Projects/Domain/Sources/Model/Auth/SignupResult.swift b/Projects/Domain/Sources/Model/Auth/SignupResult.swift new file mode 100644 index 0000000..775fabb --- /dev/null +++ b/Projects/Domain/Sources/Model/Auth/SignupResult.swift @@ -0,0 +1,21 @@ +// +// SignupResult.swift +// Domain +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct SignupResult: Sendable { + public let uuid: String + public let accessToken: String + public let nickname: String + + public init(uuid: String, accessToken: String, nickname: String) { + self.uuid = uuid + self.accessToken = accessToken + self.nickname = nickname + } +} diff --git a/Projects/Modules/Networks/Project.swift b/Projects/Modules/Networks/Project.swift index 0d7fe6a..aab2aec 100644 --- a/Projects/Modules/Networks/Project.swift +++ b/Projects/Modules/Networks/Project.swift @@ -16,7 +16,8 @@ let project = Project.makeModule( .makeFrameworkTarget( name: "Networks", dependencies: [ - .core + .core, + .domain ], scripts: [.swiftLint], isStatic: true, diff --git a/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift b/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift deleted file mode 100644 index 03fd382..0000000 --- a/Projects/Modules/Networks/Sources/Base/APIErrorProtocol.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// APIErrorProtocol.swift -// Networks -// -// Created by kimnahun on 1/19/26. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Foundation - -public protocol APIErrorProtocol: Error { - var message: String { get } - - init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) -} diff --git a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift index efbf298..f2c5593 100644 --- a/Projects/Modules/Networks/Sources/Base/NetworkResult.swift +++ b/Projects/Modules/Networks/Sources/Base/NetworkResult.swift @@ -7,9 +7,9 @@ // import Foundation - + @frozen -public enum NetworkResult: Sendable { +public enum NetworkResult: Sendable { case success(T) case failure(E) case networkFailure(NetworkError) diff --git a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift b/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift deleted file mode 100644 index 01aa2b1..0000000 --- a/Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// SignupError.swift -// Networks -// -// Created by kimnahun on 1/19/26. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Foundation - -public enum SignupField: Sendable { - case fcmToken - case unknown(String) - - public init(rawValue: String) { - switch rawValue { - case "fcmToken": self = .fcmToken - default: self = .unknown(rawValue) - } - } -} - -// MARK: - Error -public struct SignupError: APIErrorProtocol, Sendable { - public let type: ErrorType - public let message: String - public let field: SignupField? - public let fieldMessage: String? - - public enum ErrorType: Sendable { - case validationError - case internalServerError - case undefined(code: String) - } - - public init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) { - self.message = message - - if let firstError = errors.first { - self.field = SignupField(rawValue: firstError.field ?? "") - self.fieldMessage = firstError.message - } else { - self.field = nil - self.fieldMessage = nil - } - - switch code { - case "COMM-01-005": self.type = .validationError - case "COMM-08-001": self.type = .internalServerError - default: self.type = .undefined(code: code) - } - } -} diff --git a/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift b/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift new file mode 100644 index 0000000..9d150b4 --- /dev/null +++ b/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift @@ -0,0 +1,48 @@ +// +// SignupError+Mapping.swift +// Networks +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain +import Foundation + +extension SignupError { + public init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) { + let fieldMessage = errors.first?.message ?? message + + let field: SignupField = { + guard let fieldString = errors.first?.field else { + return .unknown("unknown") + } + switch fieldString { + case "fcmToken": return .fcmToken + case "email": return .email + default: return .unknown(fieldString) + } + }() + + switch code { + case "COMM-01-005": + self = .validationFailed(field: field, message: fieldMessage) + case "AUTH-01-001": + self = .userAlreadyExists + case "COMM-08-001": + self = .serverError(message: message) + default: + self = .unknown(code: code, message: message) + } + } +} + +extension SignupField { + public init(rawValue: String) { + switch rawValue { + case "fcmToken": self = .fcmToken + case "email": self = .email + default: self = .unknown(rawValue) + } + } +} diff --git a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift index fad933d..b058195 100644 --- a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift +++ b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift @@ -11,10 +11,10 @@ import Moya extension MoyaProvider { private static var successCode: String { "2000" } - - func request( + + func request( _ target: Target, - errorType: E.Type + errorMapper: @escaping @Sendable (String, String, [ErrorResponse.ErrorDetail]) -> E ) async -> NetworkResult { await withCheckedContinuation { continuation in self.request(target) { result in @@ -25,7 +25,7 @@ extension MoyaProvider { BaseResponse.self, from: response.data ) - + if baseResponse.code == Self.successCode { guard let data = baseResponse.data else { continuation.resume(returning: .networkFailure(.decodingFailed)) @@ -37,17 +37,17 @@ extension MoyaProvider { ErrorResponse.self, from: response.data ) - let apiError = E( - code: errorResponse.code ?? "", - message: errorResponse.message ?? "", - errors: errorResponse.errors ?? [] + let apiError = errorMapper( + errorResponse.code ?? "", + errorResponse.message ?? "", + errorResponse.errors ?? [] ) continuation.resume(returning: .failure(apiError)) } } catch { continuation.resume(returning: .networkFailure(.decodingFailed)) } - + case .failure(let moyaError): let networkError = Self.mapMoyaError(moyaError) continuation.resume(returning: .networkFailure(networkError)) @@ -55,10 +55,10 @@ extension MoyaProvider { } } } - - func requestPlain( + + func requestPlain( _ target: Target, - errorType: E.Type + errorMapper: @escaping @Sendable (String, String, [ErrorResponse.ErrorDetail]) -> E ) async -> NetworkResult { await withCheckedContinuation { continuation in self.request(target) { result in @@ -69,7 +69,7 @@ extension MoyaProvider { BaseResponse.self, from: response.data ) - + if baseResponse.code == Self.successCode { continuation.resume(returning: .success(())) } else { @@ -77,17 +77,17 @@ extension MoyaProvider { ErrorResponse.self, from: response.data ) - let apiError = E( - code: errorResponse.code ?? "", - message: errorResponse.message ?? "", - errors: errorResponse.errors ?? [] + let apiError = errorMapper( + errorResponse.code ?? "", + errorResponse.message ?? "", + errorResponse.errors ?? [] ) continuation.resume(returning: .failure(apiError)) } } catch { continuation.resume(returning: .networkFailure(.decodingFailed)) } - + case .failure(let moyaError): let networkError = Self.mapMoyaError(moyaError) continuation.resume(returning: .networkFailure(networkError)) @@ -95,7 +95,7 @@ extension MoyaProvider { } } } - + private static func mapMoyaError(_ error: MoyaError) -> NetworkError { switch error { case .underlying(let nsError as NSError, _) diff --git a/Projects/Modules/Networks/Sources/Service/AuthService.swift b/Projects/Modules/Networks/Sources/Service/AuthService.swift index 847cd1c..3dc7084 100644 --- a/Projects/Modules/Networks/Sources/Service/AuthService.swift +++ b/Projects/Modules/Networks/Sources/Service/AuthService.swift @@ -6,14 +6,15 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // +import Domain import Foundation import Moya -public protocol AuthServiceProtocol { +public protocol AuthServiceProtocol: Sendable { func signup(request: SignupRequest) async -> NetworkResult } -public final class AuthService: AuthServiceProtocol { +public final class AuthService: AuthServiceProtocol, @unchecked Sendable { private let provider: MoyaProvider public init(provider: MoyaProvider = MoyaProvider()) { @@ -21,6 +22,6 @@ public final class AuthService: AuthServiceProtocol { } public func signup(request: SignupRequest) async -> NetworkResult { - await provider.request(.signup(request: request), errorType: SignupError.self) + await provider.request(.signup(request: request), errorMapper: SignupError.init) } } From 963d84238474c9d1bfb84df6eb31add399b253f1 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Wed, 21 Jan 2026 01:42:09 +0900 Subject: [PATCH 13/14] =?UTF-8?q?del:=20#3=20-=20userAlreadyExists=20case?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Project.swift | 1 + Projects/Domain/Sources/Model/Auth/SignupError.swift | 1 - .../Networks/Sources/ErrorMapping/SignupError+Mapping.swift | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index eb201ca..c029072 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -21,6 +21,7 @@ let project = Project.makeModule( scripts: [.swiftLint], dependencies: [ .data, + .Modules.networks, .Features.rootFeature ], settings: .appSettings() diff --git a/Projects/Domain/Sources/Model/Auth/SignupError.swift b/Projects/Domain/Sources/Model/Auth/SignupError.swift index fe1f7a6..c0dcd9b 100644 --- a/Projects/Domain/Sources/Model/Auth/SignupError.swift +++ b/Projects/Domain/Sources/Model/Auth/SignupError.swift @@ -20,7 +20,6 @@ public enum SignupField: Sendable, Equatable { public enum SignupError: Error, Sendable { case validationFailed(field: SignupField, message: String) - case userAlreadyExists case serverError(message: String?) case networkError(message: String) case unknown(code: String, message: String) diff --git a/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift b/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift index 9d150b4..2bd5200 100644 --- a/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift +++ b/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift @@ -27,8 +27,6 @@ extension SignupError { switch code { case "COMM-01-005": self = .validationFailed(field: field, message: fieldMessage) - case "AUTH-01-001": - self = .userAlreadyExists case "COMM-08-001": self = .serverError(message: message) default: From a6c41a3c02fb585b066315f23362d9c2f116c2b8 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Wed, 21 Jan 2026 13:51:09 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20#3=20-=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20Logger=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/MoyaProvider+Async.swift | 14 +++- .../Sources/Logger/NetworkLogger.swift | 75 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift diff --git a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift index b058195..5599e7a 100644 --- a/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift +++ b/Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift @@ -16,10 +16,14 @@ extension MoyaProvider { _ target: Target, errorMapper: @escaping @Sendable (String, String, [ErrorResponse.ErrorDetail]) -> E ) async -> NetworkResult { - await withCheckedContinuation { continuation in + NetworkLogger.logRequest(target) + + return await withCheckedContinuation { continuation in self.request(target) { result in switch result { case .success(let response): + NetworkLogger.logResponse(response) + do { let baseResponse = try JSONDecoder().decode( BaseResponse.self, @@ -49,6 +53,7 @@ extension MoyaProvider { } case .failure(let moyaError): + NetworkLogger.logError(moyaError) let networkError = Self.mapMoyaError(moyaError) continuation.resume(returning: .networkFailure(networkError)) } @@ -60,10 +65,14 @@ extension MoyaProvider { _ target: Target, errorMapper: @escaping @Sendable (String, String, [ErrorResponse.ErrorDetail]) -> E ) async -> NetworkResult { - await withCheckedContinuation { continuation in + NetworkLogger.logRequest(target) + + return await withCheckedContinuation { continuation in self.request(target) { result in switch result { case .success(let response): + NetworkLogger.logResponse(response) + do { let baseResponse = try JSONDecoder().decode( BaseResponse.self, @@ -89,6 +98,7 @@ extension MoyaProvider { } case .failure(let moyaError): + NetworkLogger.logError(moyaError) let networkError = Self.mapMoyaError(moyaError) continuation.resume(returning: .networkFailure(networkError)) } diff --git a/Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift b/Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift new file mode 100644 index 0000000..4789893 --- /dev/null +++ b/Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift @@ -0,0 +1,75 @@ +// +// NetworkLogger.swift +// Networks +// +// Created by kimnahun on 1/21/26. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation +import Moya + +enum NetworkLogger { + + static func logRequest(_ target: TargetType) { + #if DEBUG + print("──────────────────────────────────────") + print("[REQUEST] \(target.method.rawValue) \(target.baseURL)\(target.path)") + if let body = requestBody(from: target.task) { + print("[BODY] \(body)") + } + print("──────────────────────────────────────") + #endif + } + + static func logResponse(_ response: Response) { + #if DEBUG + let statusCode = response.statusCode + let url = response.request?.url?.absoluteString ?? "" + + print("──────────────────────────────────────") + print("[RESPONSE] \(statusCode) \(url)") + if let json = try? JSONSerialization.jsonObject(with: response.data), + let prettyData = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted), + let prettyString = String(data: prettyData, encoding: .utf8) { + print("[DATA]\n\(prettyString)") + } + print("──────────────────────────────────────") + #endif + } + + static func logError(_ error: MoyaError) { + #if DEBUG + print("──────────────────────────────────────") + print("[ERROR] \(error.localizedDescription)") + print("──────────────────────────────────────") + #endif + } + + private static func requestBody(from task: Moya.Task) -> String? { + switch task { + case .requestParameters(let parameters, _): + return parameters.description + case .requestJSONEncodable(let encodable): + if let data = try? JSONEncoder().encode(AnyEncodable(encodable)), + let string = String(data: data, encoding: .utf8) { + return string + } + return nil + default: + return nil + } + } +} + +private struct AnyEncodable: Encodable { + private let encode: (Encoder) throws -> Void + + init(_ value: T) { + encode = value.encode + } + + func encode(to encoder: Encoder) throws { + try encode(encoder) + } +}