@@ -3,13 +3,20 @@ import Foundation
33protocol UserServicing {
44 func fetchMyUser( ) async throws -> UserModel
55 func fetchUserStatics( userId: Int ) async throws -> UserStaticsModel
6+ func deleteMyProfileImage( ) async throws -> UserModel
7+ func issuePresignedURL( ) async throws -> PresignedURLResponse
8+ func uploadImageToPresignedURL( imageData: Data , presignedURL: URL ) async throws
9+ func updateMyProfileImage( request: UpdateMyProfileImageRequest ) async throws -> UserModel
10+ func updateMyTag( tag: String ) async throws -> UserModel
611}
712
813enum UserServiceError : LocalizedError {
914 case invalidResponse
1015 case serverError( statusCode: Int , message: String ? )
1116 case decodingFailed
17+ case requestEncodingFailed
1218 case sessionExpired
19+ case uploadFailed( statusCode: Int , message: String ? )
1320 case networkFailure( message: String )
1421
1522 var errorDescription : String ? {
@@ -20,8 +27,12 @@ enum UserServiceError: LocalizedError {
2027 return message ?? " 요청 처리에 실패했어요. "
2128 case . decodingFailed:
2229 return " 응답 파싱에 실패했어요. "
30+ case . requestEncodingFailed:
31+ return " 요청 생성에 실패했어요. "
2332 case . sessionExpired:
2433 return " 세션이 만료되었어요. 다시 로그인해 주세요. "
34+ case . uploadFailed( _, let message) :
35+ return message ?? " 프로필 이미지 업로드에 실패했어요. "
2536 case . networkFailure( let message) :
2637 return message
2738 }
@@ -30,9 +41,14 @@ enum UserServiceError: LocalizedError {
3041
3142struct UserService : UserServicing {
3243 private let apiClient : APIClienting
44+ private let session : URLSession
3345
34- init ( apiClient: APIClienting = APIClient . shared) {
46+ init (
47+ apiClient: APIClienting = APIClient . shared,
48+ session: URLSession = . shared
49+ ) {
3550 self . apiClient = apiClient
51+ self . session = session
3652 }
3753
3854 func fetchMyUser( ) async throws -> UserModel {
@@ -63,6 +79,107 @@ struct UserService: UserServicing {
6379 }
6480 }
6581
82+ func deleteMyProfileImage( ) async throws -> UserModel {
83+ do {
84+ let request = APIRequest (
85+ path: " /users/my/profile-image " ,
86+ method: . delete,
87+ requiresAuthorization: true
88+ )
89+ return try await apiClient. request ( request, responseType: UserModel . self)
90+ } catch {
91+ if isRequestCancelled ( error) { throw error }
92+ throw mapError ( error)
93+ }
94+ }
95+
96+ func issuePresignedURL( ) async throws -> PresignedURLResponse {
97+ do {
98+ let request = APIRequest (
99+ path: " /presigned-url " ,
100+ method: . get,
101+ requiresAuthorization: true
102+ )
103+ return try await apiClient. request ( request, responseType: PresignedURLResponse . self)
104+ } catch {
105+ if isRequestCancelled ( error) { throw error }
106+ throw mapError ( error)
107+ }
108+ }
109+
110+ func uploadImageToPresignedURL( imageData: Data , presignedURL: URL ) async throws {
111+ var request = URLRequest ( url: presignedURL)
112+ request. httpMethod = HTTPMethod . put. rawValue
113+ request. httpBody = imageData
114+
115+ do {
116+ let ( data, response) = try await session. data ( for: request)
117+ guard let httpResponse = response as? HTTPURLResponse else {
118+ throw UserServiceError . invalidResponse
119+ }
120+ guard ( 200 ..< 300 ) . contains ( httpResponse. statusCode) else {
121+ throw UserServiceError . uploadFailed (
122+ statusCode: httpResponse. statusCode,
123+ message: responseMessage ( from: data)
124+ )
125+ }
126+ } catch {
127+ if isRequestCancelled ( error) { throw error }
128+ if let userServiceError = error as? UserServiceError {
129+ throw userServiceError
130+ }
131+ throw UserServiceError . networkFailure ( message: " 프로필 이미지 업로드 중 네트워크 오류가 발생했어요. " )
132+ }
133+ }
134+
135+ func updateMyProfileImage( request: UpdateMyProfileImageRequest ) async throws -> UserModel {
136+ let requestBody : Data
137+ do {
138+ requestBody = try JSONEncoder ( ) . encode ( request)
139+ } catch {
140+ throw UserServiceError . requestEncodingFailed
141+ }
142+
143+ do {
144+ var apiRequest = APIRequest (
145+ path: " /users/my/profile-image " ,
146+ method: . patch,
147+ requiresAuthorization: true ,
148+ body: requestBody
149+ )
150+ apiRequest. headers [ " Accept " ] = " application/json "
151+ apiRequest. headers [ " Content-Type " ] = " application/json "
152+ return try await apiClient. request ( apiRequest, responseType: UserModel . self)
153+ } catch {
154+ if isRequestCancelled ( error) { throw error }
155+ throw mapError ( error)
156+ }
157+ }
158+
159+ func updateMyTag( tag: String ) async throws -> UserModel {
160+ let requestBody : Data
161+ do {
162+ requestBody = try JSONEncoder ( ) . encode ( UpdateMyTagRequest ( tag: tag) )
163+ } catch {
164+ throw UserServiceError . requestEncodingFailed
165+ }
166+
167+ do {
168+ var apiRequest = APIRequest (
169+ path: " /users/my/tags " ,
170+ method: . patch,
171+ requiresAuthorization: true ,
172+ body: requestBody
173+ )
174+ apiRequest. headers [ " Accept " ] = " application/json "
175+ apiRequest. headers [ " Content-Type " ] = " application/json "
176+ return try await apiClient. request ( apiRequest, responseType: UserModel . self)
177+ } catch {
178+ if isRequestCancelled ( error) { throw error }
179+ throw mapError ( error)
180+ }
181+ }
182+
66183 private func mapError( _ error: Error ) -> UserServiceError {
67184 if let userServiceError = error as? UserServiceError {
68185 return userServiceError
@@ -75,12 +192,76 @@ struct UserService: UserServicing {
75192 case . missingAccessToken, . missingRefreshToken, . unauthorized:
76193 return . sessionExpired
77194 case . serverError( let statusCode, let message) :
78- return . serverError( statusCode: statusCode, message: message)
195+ return . serverError(
196+ statusCode: statusCode,
197+ message: normalizeServerErrorMessage ( message)
198+ )
79199 case . decodingFailed:
80200 return . decodingFailed
81201 }
82202 }
83203
84204 return . networkFailure( message: " 네트워크 요청 중 오류가 발생했어요. " )
85205 }
206+
207+ private func normalizeServerErrorMessage( _ rawMessage: String ? ) -> String ? {
208+ guard
209+ let rawMessage = rawMessage? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
210+ !rawMessage. isEmpty
211+ else {
212+ return nil
213+ }
214+
215+ guard
216+ rawMessage. first == " { " ,
217+ let data = rawMessage. data ( using: . utf8) ,
218+ let parsed = try ? JSONDecoder ( ) . decode ( UserServiceErrorResponse . self, from: data)
219+ else {
220+ return rawMessage
221+ }
222+
223+ if let message = parsed. message? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
224+ !message. isEmpty {
225+ return message
226+ }
227+
228+ let fieldMessages = ( parsed. fieldErrors ?? [ ] )
229+ . flatMap ( \. values)
230+ . map { $0. trimmingCharacters ( in: . whitespacesAndNewlines) }
231+ . filter { !$0. isEmpty }
232+
233+ let globalMessages = ( parsed. globalErrors ?? [ ] )
234+ . map { $0. trimmingCharacters ( in: . whitespacesAndNewlines) }
235+ . filter { !$0. isEmpty }
236+
237+ let merged = fieldMessages + globalMessages
238+ guard !merged. isEmpty else { return rawMessage }
239+ return merged. joined ( separator: " \n " )
240+ }
241+
242+ private func responseMessage( from data: Data ) -> String ? {
243+ guard !data. isEmpty else { return nil }
244+ guard let body = String ( data: data, encoding: . utf8) ?
245+ . trimmingCharacters ( in: . whitespacesAndNewlines) ,
246+ !body. isEmpty
247+ else {
248+ return nil
249+ }
250+ return body
251+ }
252+
253+ private func isRequestCancelled( _ error: Error ) -> Bool {
254+ if error is CancellationError {
255+ return true
256+ }
257+
258+ let nsError = error as NSError
259+ return nsError. domain == NSURLErrorDomain && nsError. code == NSURLErrorCancelled
260+ }
261+ }
262+
263+ private struct UserServiceErrorResponse : Decodable {
264+ let message : String ?
265+ let fieldErrors : [ [ String : String ] ] ?
266+ let globalErrors : [ String ] ?
86267}
0 commit comments