Skip to content

Commit 712afb7

Browse files
authored
feat - 프로필 설정 작업 (#44) (#45)
* refactor(MyCollectionProfileSettings): 프로필 설정 페이지 분리작업 * feat(ProfileCard): 팬덤, PICKS 비활성화 * feat(ProfileSetting): 프로필 이미지 변경, 삭제, 태그 수정 API 연동 * feat(ProfileSetting): 프로필 설정 페이지 UI/UX 업데이트 * feat(ProfileSetting): UI 디테일 수정 * feat(ProfileSetting): 태그 수정 시 도움말 추가 * refactor(ProfileSetting): 리팩토링 * feat(ProfileSetting): 여백 설정 * feat(1.0.18): 버전 업데이트
2 parents b1a753e + fe111b7 commit 712afb7

18 files changed

Lines changed: 944 additions & 84 deletions

KillingPart.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
435435
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
436436
CODE_SIGN_STYLE = Automatic;
437-
CURRENT_PROJECT_VERSION = 16;
437+
CURRENT_PROJECT_VERSION = 18;
438438
DEAD_CODE_STRIPPING = YES;
439439
DEVELOPMENT_TEAM = GQ89YG5G9R;
440440
ENABLE_APP_SANDBOX = YES;
@@ -459,7 +459,7 @@
459459
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
460460
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
461461
MACOSX_DEPLOYMENT_TARGET = 14.0;
462-
MARKETING_VERSION = 1.0.16;
462+
MARKETING_VERSION = 1.0.18;
463463
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
464464
PRODUCT_NAME = "$(TARGET_NAME)";
465465
REGISTER_APP_GROUPS = YES;
@@ -479,7 +479,7 @@
479479
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
480480
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
481481
CODE_SIGN_STYLE = Automatic;
482-
CURRENT_PROJECT_VERSION = 16;
482+
CURRENT_PROJECT_VERSION = 18;
483483
DEAD_CODE_STRIPPING = YES;
484484
DEVELOPMENT_TEAM = GQ89YG5G9R;
485485
ENABLE_APP_SANDBOX = YES;
@@ -504,7 +504,7 @@
504504
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
505505
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
506506
MACOSX_DEPLOYMENT_TARGET = 14.0;
507-
MARKETING_VERSION = 1.0.16;
507+
MARKETING_VERSION = 1.0.18;
508508
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
509509
PRODUCT_NAME = "$(TARGET_NAME)";
510510
REGISTER_APP_GROUPS = YES;

KillingPart/Models/UserModel.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,17 @@ struct UserStaticsModel: Decodable {
3030
let pickCount: Int
3131
let killingPartCount: Int
3232
}
33+
34+
struct PresignedURLResponse: Decodable {
35+
let id: Int
36+
let presignedUrl: String
37+
}
38+
39+
struct UpdateMyProfileImageRequest: Encodable {
40+
let id: Int
41+
let presignedUrl: String
42+
}
43+
44+
struct UpdateMyTagRequest: Encodable {
45+
let tag: String
46+
}

KillingPart/Services/UserService.swift

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ import Foundation
33
protocol 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

813
enum 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

3142
struct 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
}

KillingPart/ViewModels/My/MyCollection/MyCollectionViewModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ final class MyCollectionViewModel: ObservableObject {
139139
myFeeds.removeAll { $0.diaryId == diaryId }
140140
}
141141

142+
func applyUpdatedUser(_ updatedUser: UserModel) {
143+
user = updatedUser
144+
hasLoadedProfile = true
145+
}
146+
142147
func logout(onSuccess: @escaping () -> Void) {
143148
guard !isProcessing else { return }
144149

0 commit comments

Comments
 (0)