diff --git a/Projects/Core/Sources/Utility/UI/Keyboard/DismissKeyboardOnTap.swift b/Projects/Core/Sources/Utility/UI/Keyboard/DismissKeyboardOnTap.swift new file mode 100644 index 00000000..b297a72a --- /dev/null +++ b/Projects/Core/Sources/Utility/UI/Keyboard/DismissKeyboardOnTap.swift @@ -0,0 +1,27 @@ +// +// DismissKeyboardOnTap.swift +// Core +// +// Created by Young Bin on 2023/08/24. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import SwiftUI + +public struct DismissKeyboardOnTap: ViewModifier { + public init() {} + + public func body(content: Content) -> some View { + content + .gesture( + TapGesture().onEnded { + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), + to: nil, + from: nil, + for: nil + ) + } + ) + } +} diff --git a/Projects/Domain/Sources/User/KeymeUserStorage.swift b/Projects/Domain/Sources/User/KeymeUserStorage.swift index a516004e..e7a19a1f 100644 --- a/Projects/Domain/Sources/User/KeymeUserStorage.swift +++ b/Projects/Domain/Sources/User/KeymeUserStorage.swift @@ -13,55 +13,58 @@ protocol StorageKeyType { } public final class KeymeUserStorage { - @Dependency(\.localStorage) private var localStorage - - var nickname: String? - var acesssToken: String? + private let localStorage: CoreLocalStorage - public init() { - self.nickname = self.get(.nickname) - self.acesssToken = self.get(.acesssToken) + init(localStorage: CoreLocalStorage) { + self.localStorage = localStorage + } + + private func get(_ key: UserStorageKey) -> Any? { + localStorage.get(key) } - public func get(_ key: UserStorageKey) -> T? { - return localStorage.get(key) as? T + private func set(_ value: Any?, forKey key: UserStorageKey) { + localStorage.set(value, forKey: key) } - public func set(_ value: T?, forKey key: UserStorageKey) { - switch key { - case .acesssToken: - guard let flag = value as? String else { return } - self.acesssToken = flag - case .nickname: - guard let name = value as? String else { return } - self.nickname = name + private enum UserStorageKey: String, StorageKeyType { + case accessToken + case nickname + case profileImageURL + case profileThumbnailURL + + public var name: String { + return "UserStorageKey_\(self.rawValue)" } - - localStorage.set(value, forKey: key) } } public extension KeymeUserStorage { - enum UserStorageKey: StorageKeyType { - case acesssToken - case nickname - - var name: String { - switch self { - case .acesssToken: - return "UserStorageKey_isLoggedIn" - case .nickname: - return "UserStorageKey_nickname" - } - } + var accessToken: String? { + get { get(.accessToken) as? String } + set { set(newValue, forKey: .accessToken) } + } + + var nickname: String? { + get { get(.nickname) as? String } + set { set(newValue, forKey: .nickname) } + } + + var profileImageURL: URL? { + get { get(.profileImageURL) as? URL } + set { set(newValue, forKey: .profileImageURL) } + } + + var profileThumbnailURL: URL? { + get { get(.profileThumbnailURL) as? URL } + set { set(newValue, forKey: .profileThumbnailURL) } } } extension KeymeUserStorage: DependencyKey { - public static var liveValue = KeymeUserStorage() - public static var testValue: KeymeUserStorage { - KeymeUserStorage() // FIXME: - } + public static var liveValue = KeymeUserStorage(localStorage: CoreLocalStorage.liveValue) + public static var testValue = KeymeUserStorage( + localStorage: CoreLocalStorage.testValue(storage: .init(suiteName: "TestStorage")!)) } extension DependencyValues { diff --git a/Projects/Features/Sources/Registration/Modifiers.swift b/Projects/Features/Sources/Registration/Modifiers.swift new file mode 100644 index 00000000..4ae175a3 --- /dev/null +++ b/Projects/Features/Sources/Registration/Modifiers.swift @@ -0,0 +1,33 @@ +// +// Modifiers.swift +// Features +// +// Created by 고도현 on 2023/08/12. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import SwiftUI + +// 최대 글자 수를 넘기면 좌, 우로 떨리는 애니메이션 +struct Shake: ViewModifier { + @Binding var isShake: Bool + + func body(content: Content) -> some View { + content + .offset(x: isShake ? -10 : 0) // 좌측으로 이동 + .animation( + Animation + .easeInOut(duration: 0.1) + .repeatCount(3, autoreverses: true), + value: isShake) + .onChange(of: isShake) { newValue in + if !newValue { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + withAnimation { + self.isShake = false + } + } + } + } + } +} diff --git a/Projects/Features/Sources/Registration/RegisterFeature.swift b/Projects/Features/Sources/Registration/RegisterFeature.swift new file mode 100644 index 00000000..9f34c452 --- /dev/null +++ b/Projects/Features/Sources/Registration/RegisterFeature.swift @@ -0,0 +1,128 @@ +// +// RegisterFeature.swift +// Features +// +// Created by 이영빈 on 2023/08/23. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Foundation +import ComposableArchitecture +import Network + +public struct RegistrationFeature: Reducer { + @Dependency(\.keymeAPIManager) var network + @Dependency(\.continuousClock) var clock + + enum CancelID { case debouncedNicknameUpdate } + + public init() {} + + public struct State: Equatable { + var status: Status = .notDetermined + var isNicknameAvailable: Bool? + var canRegister: Bool { + return isNicknameAvailable == true + } + + var thumbnailURL: URL? + var originalImageURL: URL? + + var nicknameTextFieldString: String = "" + + enum Status: Equatable { + case notDetermined + case needsRegister + case complete + } + } + + public enum Action: Equatable { + case debouncedNicknameUpdate(text: String) + + case checkDuplicatedNickname(String) + case checkDuplicatedNicknameResponse(Bool) + + case registerProfileImage(Data) + case registerProfileImageResponse(thumbnailURL: URL, originalImageURL: URL) + + case finishRegister(nickname: String, thumbnailURL: URL?, originalImageURL: URL?) + case finishRegisterResponse(id: Int, friendCode: String) + } + + public var body: some Reducer { + Reduce { state, action in + switch action { + case .debouncedNicknameUpdate(let nicknameString): + state.nicknameTextFieldString = nicknameString + return .run { send in + try await withTaskCancellation( + id: CancelID.debouncedNicknameUpdate, + cancelInFlight: true + ) { + try await self.clock.sleep(for: .seconds(0.7)) + + await send(.checkDuplicatedNickname(nicknameString)) + } + } + + // MARK: checkDuplicatedNickname + case .checkDuplicatedNickname(let nickname): + return .run(priority: .userInitiated) { send in + let result = try await network.request( + .registration(.checkDuplicatedNickname(nickname)), + object: VerifyNicknameDTO.self + ) + + await send(.checkDuplicatedNicknameResponse(result.data.valid)) + } + + case .checkDuplicatedNicknameResponse(let isNicknameDuplicated): + state.isNicknameAvailable = isNicknameDuplicated + + // MARK: registerProfileImage + case .registerProfileImage(let imageData): + return .run { send in + let result = try await network.request( + .registration(.uploadImage(imageData)), + object: ImageUploadDTO.self) + + if + let thumbnailURL = URL(string: result.data.thumbnailUrl), + let originalImageURL = URL(string: result.data.originalUrl) + { + await send( + .registerProfileImageResponse( + thumbnailURL: thumbnailURL, + originalImageURL: originalImageURL)) + } + } + + case .registerProfileImageResponse(let thumnailURL, let originalImageURL): + state.thumbnailURL = thumnailURL + state.originalImageURL = originalImageURL + + // MARK: finishRegister + case .finishRegister(let nickname, let thumbnailURL, let originalImageURL): + return .run { send in + let result = try await network.request( + .registration(.updateMemberDetails( + nickname: nickname, + profileImage: thumbnailURL?.absoluteString, + profileThumbnail: originalImageURL?.absoluteString)), + object: MemberUpdateDTO.self) + + await send( + .finishRegisterResponse( + id: result.data.id, + friendCode: result.data.friendCode ?? "")) // TODO: 나중에 non-null 값 필요 + } + + case .finishRegisterResponse: + state.status = .complete + } + + return .none + } + } +} diff --git a/Projects/Features/Sources/Registration/RegistrationView.swift b/Projects/Features/Sources/Registration/RegistrationView.swift new file mode 100644 index 00000000..377b654c --- /dev/null +++ b/Projects/Features/Sources/Registration/RegistrationView.swift @@ -0,0 +1,194 @@ +// +// RegistrationView.swift +// Features +// +// Created by 고도현 on 2023/08/12. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Core +import SwiftUI +import PhotosUI + +import ComposableArchitecture + +/// 회원가입 이후, 프로필 이미지와 닉네임을 등록하는 페이지입니다. +/// - 뷰의 상단부터 순차적으로 구현하였으며 각각의 컴포넌트별로 구분할 수 있게끔 주석을 달아놓았으니 참고하시면 됩니다. +public struct RegistrationView: View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + // 닉네임 관련 프로퍼티 + @State private var nickname = "" // 사용자가 새롭게 입력한 닉네임 + @State private var beforeNickname = "" // 기존에 입력했던 닉네임 + @State private var isShake = false // 최대 글자 수를 넘긴 경우 좌, 우로 떨리는 애니메이션 + + // 프로필 이미지 관련 프로퍼티들 + @State private var selectedImage: PhotosPickerItem? + @State private var selectedImageData: Data? + + public var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + VStack(spacing: 12) { + // 프로필 이미지를 등록하는 Circle + PhotosPicker(selection: $selectedImage, matching: .images, photoLibrary: .shared()) { + profileImage(imageData: selectedImageData) + } + .onChange(of: selectedImage) { newImage in + Task(priority: .utility) { + guard let imageData = try await newImage?.loadTransferable(type: Data.self) else { + // TODO: Throw error and show alert + return + } + + viewStore.send(.registerProfileImage(imageData)) + DispatchQueue.main.async { selectedImageData = imageData } + } + } + + // 닉네임 관련 안내메세지 + HStack(alignment: .center, spacing: 4) { + Text("닉네임") + .font(.system(size: 14)) + + Text("(\(nickname.count)/6)") + .font(.system(size: 12)) + .foregroundColor(.gray) + + Spacer() + + Text("2~6자리 한글, 영어, 숫자") + .font(.system(size: 12)) + } + .padding(.horizontal, 2) + + // 닉네임을 입력하는 TextField + TextField("닉네임을 입력해주세요.", text: $nickname) + .font(.system(size: 16)) + .frame(height: 50) + .padding(.horizontal) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(.gray, lineWidth: 1) + ) + .modifier(Shake(isShake: $isShake)) + + if !nickname.isEmpty, let isValid = viewStore.isNicknameAvailable { + ValidateNicknameView(isValid: isValid) + } + + // 닉네임 관련 안내메세지 + Rectangle() + .frame(height: 80) + .foregroundColor(.gray) + .cornerRadius(8) + .overlay( + Text("친구들이 원할하게 문제를 풀 수 있도록, 나를 가장 잘 나타내는 닉네임으로 설정해주세요. \(viewStore.canRegister.description)") + .font(.system(size: 14)) + .fontWeight(.semibold) + .foregroundColor(.white) + .padding(.horizontal, 8) + ) + + Spacer() + + // 다음 페이지로 넘어가기 위한 Button + Button(action: { + viewStore.send( + .finishRegister( + nickname: viewStore.state.nicknameTextFieldString, + thumbnailURL: viewStore.thumbnailURL, + originalImageURL: viewStore.originalImageURL)) + }) { + HStack { + Spacer() + + Text("다음") + .font(.system(size: 18)) + .fontWeight(.bold) + .frame(height: 60) + + Spacer() + } + } + .foregroundColor(.white) + .background(viewStore.canRegister ? .black : .gray) + .cornerRadius(16) + .disabled(viewStore.canRegister ? false : true) + } + .modifier(DismissKeyboardOnTap()) + .padding(.horizontal, 16) + .onChange(of: nickname) { newValue in + guard 1 <= newValue.count, newValue.count <= 6 else { + if newValue.count > 6 { // 최대 글자 수를 넘겼으므로 Shake Start + isShake = true + nickname = beforeNickname // 최대 글자 수를 넘기기 전에 입력한 닉네임으로 고정 + } + + return + } + + isShake = false + beforeNickname = newValue + + viewStore.send(.debouncedNicknameUpdate(text: newValue)) + } + } + } +} + +// 닉네임에 대한 검증 여부를 보여주는 뷰 +extension RegistrationView { + func profileImage(imageData: Data?) -> some View { + ZStack(alignment: .bottomTrailing) { + Group { + if + let selectedImageData = imageData, + let profileImage = UIImage(data: selectedImageData) + { + Image(uiImage: profileImage) + .resizable() + .scaledToFill() + } else { + Circle() + .foregroundColor(.gray) + .overlay(Circle().stroke(.white, lineWidth: 1)) + } + } + .frame(width: 160, height: 160) + .clipShape(Circle()) + + ZStack { + Circle() + .foregroundColor(.black) + .frame(width: 50, height: 50) + Image(systemName: "photo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 33.3, height: 33.3) + .foregroundColor(.white) + } + } + } + + struct ValidateNicknameView: View { + let isValid: Bool + + var body: some View { + HStack { + Image(systemName: isValid ? "checkmark.circle" : "xmark.circle") + .foregroundColor(isValid ? .green : .red) + .frame(width: 10, height: 10) + + Text.keyme(isValid ? "사용 가능한 닉네임입니다." : "중복된 닉네임입니다.", font: .caption1) + .foregroundColor(.gray) + + Spacer() + } + .padding(8) + } + } +} diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index e0f409a7..bee15aed 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -22,9 +22,15 @@ public struct RootFeature: Reducer { public struct State: Equatable { @PresentationState public var logInStatus: SignInFeature.State? + @PresentationState public var registrationState: RegistrationFeature.State? @PresentationState public var onboardingStatus: OnboardingFeature.State? - public init(isLoggedIn: Bool? = nil, doneOnboarding: Bool? = nil) { + public init( + isLoggedIn: Bool? = nil, + doneRegistration: Bool? = nil, + doneOnboarding: Bool? = nil + ) { + registrationState = .init() onboardingStatus = .init() if let isLoggedIn { @@ -33,6 +39,12 @@ public struct RootFeature: Reducer { logInStatus = .notDetermined } + if let doneRegistration { + registrationState?.status = doneRegistration ? .complete : .needsRegister + } else { + registrationState?.status = .notDetermined + } + if let doneOnboarding { onboardingStatus?.status = doneOnboarding ? .completed : .needsOnboarding } else { @@ -43,51 +55,85 @@ public struct RootFeature: Reducer { public enum Action { case login(PresentationAction) + case registration(PresentationAction) case onboarding(PresentationAction) case mainPage(MainPageFeature.Action) case onboardingChecked(TaskResult) - case logInChecked(Bool) - - case checkOnboardingStatus + case checkLoginStatus + case checkRegistrationStatus + case checkOnboardingStatus + + case updateMemberInformation } public var body: some ReducerOf { Reduce { state, action in switch action { case .login(.presented(.signInWithAppleResponse(let response))): - let token: String? switch response { case .success(let body): - token = body.data.token.accessToken + let token = body.data.token.accessToken + userStorage.accessToken = token + network.registerAuthorizationToken(token) + + if body.data.nickname == nil { + state.registrationState?.status = .needsRegister + } else { + state.registrationState?.status = .complete + } + return .none + case .failure: - token = nil + return .none } - userStorage.set(token, forKey: .acesssToken) - network.registerAuthorizationToken(token) - return .none - case .login(.presented(.signInWithKakaoResponse(let response))): - let token: String? switch response { case .success(let body): - token = body.data.token.accessToken + let token = body.data.token.accessToken + userStorage.accessToken = token + network.registerAuthorizationToken(token) + + if body.data.nickname == nil { + state.registrationState?.status = .needsRegister + } else { + state.registrationState?.status = .complete + } + return .none + case .failure: - token = nil + return .none + } + + case .checkLoginStatus: + let accessToken = userStorage.accessToken + + if accessToken == nil { + state.logInStatus = .loggedOut + } else { + state.logInStatus = .loggedIn + network.registerAuthorizationToken(accessToken) } - userStorage.set(token, forKey: .acesssToken) - network.registerAuthorizationToken(token) return .none - case .checkLoginStatus: - let isLoggedIn: Bool = userStorage.get(.acesssToken) == nil ? false : true - return .run { send in - await send(.logInChecked(isLoggedIn)) + case .registration(.presented(.finishRegisterResponse)): + // Do nothing currently + return .none + + case .checkRegistrationStatus: + let nickname: String? = userStorage.nickname + + if nickname == nil { + state.registrationState?.status = .needsRegister + } else { + state.registrationState?.status = .complete } + return .none + case .checkOnboardingStatus: return .run(priority: .userInitiated) { send in await send(.onboardingChecked( @@ -99,24 +145,36 @@ public struct RootFeature: Reducer { )) } - case .logInChecked(let result): - switch result { - case true: - state.logInStatus = .loggedIn - case false: - state.logInStatus = .loggedOut - } - return .none - case .onboardingChecked(.success(let result)): switch result { case true: state.onboardingStatus?.status = .completed + // 온보딩 체크 끝나면 서버에 API 쳐서 유저 데이터 업데이트 + // 온보딩 체크는 앱 켜질 떄마다 될 거고 이게 사실상 앱 진입 전 마지막 과정이라서 여기서 업뎃 + return .send(.updateMemberInformation) + case false: state.onboardingStatus?.status = .needsOnboarding } return .none + case .updateMemberInformation: + return .run(priority: .userInitiated) { _ in + let memberInformation = try await network.request( + .member(.fetch), + object: MemberUpdateDTO.self).data + + userStorage.nickname = memberInformation.nickname + + if let profileImageURL = URL(string: memberInformation.profileImage) { + userStorage.profileImageURL = profileImageURL + } + + if let profileThumbnailURL = URL(string: memberInformation.profileImage) { + userStorage.profileThumbnailURL = profileThumbnailURL + } + } + default: return .none } @@ -124,6 +182,9 @@ public struct RootFeature: Reducer { .ifLet(\.$logInStatus, action: /Action.login) { SignInFeature() } + .ifLet(\.$registrationState, action: /Action.registration) { + RegistrationFeature() + } .ifLet(\.$onboardingStatus, action: /Action.onboarding) { OnboardingFeature() } diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index ec96ac87..7ab65daa 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -18,7 +18,8 @@ public struct RootView: View { } store.send(.checkLoginStatus) - store.send(.checkOnboardingStatus) // For 디버깅, 의도적으로 3초 딜레이 + store.send(.checkRegistrationStatus) + store.send(.checkOnboardingStatus) // For 디버깅, 의도적으로 딜레이 } public var body: some View { @@ -35,6 +36,18 @@ public struct RootView: View { IfLetStore(loginStore) { store in SignInView(store: store) } + } else if viewStore.registrationState?.status == .notDetermined { + // 개인정보 등록 상태를 로딩 중 + ProgressView() + } else if viewStore.registrationState?.status == .needsRegister { + // 개인정보 등록 + let registrationStore = store.scope( + state: \.$registrationState, + action: RootFeature.Action.registration) + + IfLetStore(registrationStore) { store in + RegistrationView(store: store) + } } else if viewStore.onboardingStatus?.status == .notDetermined { // 온보딩 상태를 로딩 중 ProgressView() @@ -59,8 +72,8 @@ public struct RootView: View { } } - struct RootView_Previews: PreviewProvider { +struct RootView_Previews: PreviewProvider { static var previews: some View { RootView() } - } +} diff --git a/Projects/Network/Sources/DTO/ImageUploadDTO.swift b/Projects/Network/Sources/DTO/ImageUploadDTO.swift new file mode 100644 index 00000000..0b1083ea --- /dev/null +++ b/Projects/Network/Sources/DTO/ImageUploadDTO.swift @@ -0,0 +1,20 @@ +// +// ImageUploadDTO.swift +// Network +// +// Created by Young Bin on 2023/08/23. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Foundation + +public struct ImageUploadDTO: Codable { + let code: Int + public let data: ImageData + let message: String + + public struct ImageData: Codable { + public let originalUrl: String + public let thumbnailUrl: String + } +} diff --git a/Projects/Network/Sources/DTO/MemberUpdateDTO.swift b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift new file mode 100644 index 00000000..cac54c30 --- /dev/null +++ b/Projects/Network/Sources/DTO/MemberUpdateDTO.swift @@ -0,0 +1,23 @@ +// +// MemberUpdateDTO.swift +// Network +// +// Created by Young Bin on 2023/08/23. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Foundation + +public struct MemberUpdateDTO: Codable { + let code: Int + public let data: MemberData + let message: String + + public struct MemberData: Codable { + public let friendCode: String? + public let id: Int + public let nickname: String + public let profileImage: String + public let profileThumbnail: String + } +} diff --git a/Projects/Network/Sources/DTO/VerifyNicknameDTO.swift b/Projects/Network/Sources/DTO/VerifyNicknameDTO.swift new file mode 100644 index 00000000..085d8068 --- /dev/null +++ b/Projects/Network/Sources/DTO/VerifyNicknameDTO.swift @@ -0,0 +1,20 @@ +// +// VerifyNicknameDTO.swift +// Network +// +// Created by Young Bin on 2023/08/24. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Foundation + +public struct VerifyNicknameDTO: Decodable { + let code: Int + let message: String + public let data: NicknameData + + public struct NicknameData: Decodable { + let description: String + public let valid: Bool + } +} diff --git a/Projects/Network/Sources/Network/API/KeymeAPI.swift b/Projects/Network/Sources/Network/API/KeymeAPI.swift index 8c7be478..ff9e59d1 100644 --- a/Projects/Network/Sources/Network/API/KeymeAPI.swift +++ b/Projects/Network/Sources/Network/API/KeymeAPI.swift @@ -15,6 +15,8 @@ public enum KeymeAPI { case myPage(MyPage) case registerPushToken(String) case auth(Authorization) + case registration(Registration) + case member(Member) } extension KeymeAPI { @@ -35,6 +37,16 @@ extension KeymeAPI { case apple = "APPLE" } } + + public enum Registration { + case checkDuplicatedNickname(String) + case uploadImage(Data) + case updateMemberDetails(nickname: String, profileImage: String?, profileThumbnail: String?) + } + + public enum Member { + case fetch + } } extension KeymeAPI: BaseAPI { @@ -54,21 +66,31 @@ extension KeymeAPI: BaseAPI { case .auth: return "/auth/login" + + case .registration(.checkDuplicatedNickname): + return "members/verify-nickname" + + case .registration(.uploadImage): + return "/images" + + case .registration(.updateMemberDetails): + return "/members" + + case .member(.fetch): + return "members" } } public var method: Moya.Method { switch self { - case .test: - return .get - case .myPage(.statistics): + case .test, .myPage(.statistics), .member(.fetch): return .get - case .auth(.signIn): - return .post - case .registerPushToken: - return .post - case .auth: + + case .auth(.signIn), .registerPushToken, .registration(.checkDuplicatedNickname), .registration(.uploadImage): return .post + + case .registration(.updateMemberDetails): + return .patch } } @@ -76,10 +98,13 @@ extension KeymeAPI: BaseAPI { switch self { case .test: return .requestPlain + case .myPage(.statistics(_, let type)): return .requestParameters(parameters: ["type": type.rawValue], encoding: URLEncoding.default) + case .registerPushToken(let token): return .requestParameters(parameters: ["token": token], encoding: JSONEncoding.default) + case .auth(.signIn(let oauthType, let accessToken)): return .requestParameters( parameters: [ @@ -87,6 +112,28 @@ extension KeymeAPI: BaseAPI { "token": accessToken ], encoding: JSONEncoding.prettyPrinted) + + case .registration(.checkDuplicatedNickname(let nickname)): + return .requestJSONEncodable(nickname) + + case .registration(.uploadImage(let imageData)): + let multipartFormData = MultipartFormData( + provider: .data(imageData), + name: "profile_image") + + return .uploadMultipart([multipartFormData]) + + case .registration(.updateMemberDetails(let nickname, let profileImage, let profileThumbnail)): + return .requestParameters( + parameters: [ + "nickname": nickname, + "profileImage": profileImage as Any, + "profileThumbnail": profileThumbnail as Any + ], + encoding: JSONEncoding.default) + + case .member(.fetch): + return .requestPlain } }