diff --git a/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 5ca511e..98b1394 100644 --- a/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Launcher_icon_1024px.png", + "filename" : "launcher_icon_1024px.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Launcher_icon_1024px.png b/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Launcher_icon_1024px.png deleted file mode 100644 index d67350d..0000000 Binary files a/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/Launcher_icon_1024px.png and /dev/null differ diff --git a/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/launcher_icon_1024px.png b/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/launcher_icon_1024px.png new file mode 100644 index 0000000..9280e97 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/AppIcon.appiconset/launcher_icon_1024px.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_circle_x.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_circle_x.imageset/Contents.json new file mode 100644 index 0000000..500fca5 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_circle_x.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_circle_x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_circle_x.imageset/ic_circle_x.png b/SodaLive/Resources/Assets.xcassets/ic_circle_x.imageset/ic_circle_x.png new file mode 100644 index 0000000..aba76c8 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_circle_x.imageset/ic_circle_x.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_tag_check.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_tag_check.imageset/Contents.json new file mode 100644 index 0000000..17d60f9 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_tag_check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_tag_check.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_tag_check.imageset/ic_tag_check.png b/SodaLive/Resources/Assets.xcassets/ic_tag_check.imageset/ic_tag_check.png new file mode 100644 index 0000000..0243889 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_tag_check.imageset/ic_tag_check.png differ diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index efab69e..9012cda 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -95,4 +95,10 @@ enum AppStep { onClickStart: () -> Void, onClickCancel: () -> Void ) + + case modifyPassword + + case changeNickname + + case profileUpdate(refresh: () -> Void) } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 5480aae..17ac89d 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -133,6 +133,15 @@ struct ContentView: View { onClickCancel: onClickCancel ) + case .modifyPassword: + ModifyPasswordView() + + case .changeNickname: + NicknameUpdateView() + + case .profileUpdate(let refresh): + ProfileUpdateView(refresh: refresh) + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/MyPage/MyInfoCardView.swift b/SodaLive/Sources/MyPage/MyInfoCardView.swift index 0bdfbfe..3287d31 100644 --- a/SodaLive/Sources/MyPage/MyInfoCardView.swift +++ b/SodaLive/Sources/MyPage/MyInfoCardView.swift @@ -31,6 +31,7 @@ struct MyInfoCardView: View { Button(action: { if AppState.shared.roomId <= 0 { + AppState.shared.setAppStep(step: .profileUpdate(refresh: refresh)) } }) { Image("ic_myinfo_edit") diff --git a/SodaLive/Sources/MyPage/Profile/GetProfileResponse.swift b/SodaLive/Sources/MyPage/Profile/GetProfileResponse.swift new file mode 100644 index 0000000..bde8e04 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/GetProfileResponse.swift @@ -0,0 +1,24 @@ +// +// GetProfileResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation + +struct GetProfileResponse: Decodable { + let userId: Int + let email: String + let nickname: String + let gender: Gender + var profileUrl: String + let chargeCan: Int + let rewardCan: Int + let youtubeUrl: String? + let instagramUrl: String? + let blogUrl: String? + let websiteUrl: String? + let introduce: String + let tags: [String] +} diff --git a/SodaLive/Sources/MyPage/Profile/Nickname/GetChangeNicknamePriceResponse.swift b/SodaLive/Sources/MyPage/Profile/Nickname/GetChangeNicknamePriceResponse.swift new file mode 100644 index 0000000..6697e9b --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Nickname/GetChangeNicknamePriceResponse.swift @@ -0,0 +1,12 @@ +// +// GetChangeNicknamePriceResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation + +struct GetChangeNicknamePriceResponse: Decodable { + let price: Int +} diff --git a/SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateView.swift b/SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateView.swift new file mode 100644 index 0000000..b0b06c1 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateView.swift @@ -0,0 +1,110 @@ +// +// NicknameUpdateView.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import SwiftUI + +struct NicknameUpdateView: View { + + @StateObject var viewModel = NicknameUpdateViewModel() + @StateObject var keyboardHandler = KeyboardHandler() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + VStack(spacing: 0) { + DetailNavigationBar(title: "프로필 수정") + + Text("닉네임 변경으로 인해 피해를 입는 사용자가 지속적으로 발생하여 닉네임 변경을 부득이하게 유료로 전환합니다.") + .fixedSize(horizontal: false, vertical: true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 40, alignment: .leading) + .padding(.top, 40) + + Text("최초 1회에 한해서 무료로 변경이 가능하고, 그 이후부터는 유료로 전환됩니다.") + .fixedSize(horizontal: false, vertical: true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "dd4500")) + .frame(width: screenSize().width - 40, alignment: .leading) + + UserTextField( + title: "닉네임(최대 12자)", + hint: "닉네임", + isSecure: false, + variable: $viewModel.nickname + ) + .frame(width: screenSize().width - 40) + .padding(.top, 40) + + Text("중복확인") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 40) + .padding(.vertical, 13.3) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(hex: "9970ff"), lineWidth: 1) + ) + .padding(.top, 21.3) + .onTapGesture { + hideKeyboard() + viewModel.checkNickname() + } + + Spacer() + + Text(viewModel.price > 0 ? "\(viewModel.price)코인으로 닉네임 변경하기" : "닉네임 변경하기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.white) + .padding(.vertical, 16) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(.vertical, 13.7) + .frame(width: screenSize().width) + .background(Color(hex: "222222")) + .cornerRadius(16.7) + .padding(.top, 13.3) + .onTapGesture { viewModel.changeNickname() } + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color.black) + .frame(width: proxy.size.width, height: 15.3) + } + } + .edgesIgnoringSafeArea(.bottom) + .onTapGesture { + hideKeyboard() + } + } + .onAppear { + viewModel.nickname = UserDefaults.string(forKey: .nickname) + viewModel.getChangeNicknamePrice() + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .padding(.horizontal, 6.7) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + } + } +} diff --git a/SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateViewModel.swift b/SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateViewModel.swift new file mode 100644 index 0000000..864b3d9 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Nickname/NicknameUpdateViewModel.swift @@ -0,0 +1,168 @@ +// +// NicknameUpdateViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation +import Combine + +import Moya + +final class NicknameUpdateViewModel: ObservableObject { + private let repository = UserRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + @Published var nickname = "" { + didSet { + if nickname.count > 12 { + nickname = String(nickname.prefix(12)) + } + + isCheckedNickname = false + } + } + @Published var price = 0 + + var isCheckedNickname = false + + func getChangeNicknamePrice() { + isLoading = true + + repository.getChangeNicknamePrice() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.price = data.price + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + ERROR_LOG(error.localizedDescription) + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + + func checkNickname() { + if !nickname.trimmingCharacters(in: .whitespaces).isEmpty { + isLoading = true + + repository.checkNickname(nickname: nickname) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + self.isCheckedNickname = true + self.errorMessage = "사용가능한 닉네임 입니다." + self.isShowPopup = true + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } else { + self.errorMessage = "닉네임을 입력하세요." + self.isShowPopup = true + } + } + + func changeNickname() { + if isCheckedNickname { + isLoading = true + + let request = ProfileUpdateRequest(email: UserDefaults.string(forKey: .email), nickname: nickname) + repository.changeNickname(request: request) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + UserDefaults.set(nickname, forKey: .nickname) + self.errorMessage = "닉네임이 변경되었습니다." + self.isShowPopup = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AppState.shared.back() + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + + } else { + self.errorMessage = "닉네임 중복체크를 해주세요." + self.isShowPopup = true + } + } +} diff --git a/SodaLive/Sources/MyPage/Profile/Password/ModifyPasswordView.swift b/SodaLive/Sources/MyPage/Profile/Password/ModifyPasswordView.swift new file mode 100644 index 0000000..b7c586a --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Password/ModifyPasswordView.swift @@ -0,0 +1,109 @@ +// +// ModifyPasswordView.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import SwiftUI + +struct ModifyPasswordView: View { + + @StateObject var viewModel = ProfileUpdateViewModel() + @StateObject var keyboardHandler = KeyboardHandler() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + VStack(spacing: 0) { + DetailNavigationBar(title: "비밀번호 변경") + + ScrollView(.vertical, showsIndicators: false) { + Text("안전한 비밀번호로 내 내 정보를 보호하세요") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.top, 40) + + VStack(spacing: 26.7) { + UserTextField( + title: "현재 비밀번호", + hint: "현재 비밀번호를 입력하세요.", + isSecure: true, + variable: $viewModel.currentPassword + ) + + UserTextField( + title: "신규 비밀번호", + hint: "신규 비밀번호를 입력해주세요(영문, 숫자 포함 8자 이상)", + isSecure: true, + variable: $viewModel.newPassword + ) + + UserTextField( + title: "신규 비밀번호 확인", + hint: "신규 비밀번호를 재입력해주세요", + isSecure: true, + variable: $viewModel.newPasswordConfirm + ) + } + .padding(.top, 40) + .frame(width: screenSize().width - 53.4) + + Text("* 영문, 숫자 포함 8자 이상") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "dd4500")) + .frame(width: screenSize().width - 53.4, alignment: .leading) + .padding(.top, 13.7) + } + + if !viewModel.isLoading { + Text("비밀번호 변경하기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.vertical, 16) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(.vertical, 13.7) + .frame(width: screenSize().width) + .background(Color(hex: "222222")) + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .onTapGesture { + hideKeyboard() + viewModel.updatePassword() + } + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: proxy.size.width, height: 15.3) + } + } + } + .edgesIgnoringSafeArea(.bottom) + .onTapGesture { + hideKeyboard() + } + } + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .padding(.horizontal, 6.7) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + } +} diff --git a/SodaLive/Sources/MyPage/Profile/ProfileUpdateRequest.swift b/SodaLive/Sources/MyPage/Profile/ProfileUpdateRequest.swift new file mode 100644 index 0000000..d21d2e4 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/ProfileUpdateRequest.swift @@ -0,0 +1,24 @@ +// +// ProfileUpdateRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation + +struct ProfileUpdateRequest: Encodable { + let email: String + var password: String? = nil + var modifyPassword: String? = nil + var nickname: String? = nil + var gender: Gender? = nil + var introduce: String? = nil + var youtubeUrl: String? = nil + var instagramUrl: String? = nil + var websiteUrl: String? = nil + var blogUrl: String? = nil + let container: String = "ios" + var insertTags: [String]? = nil + var removeTags: [String]? = nil +} diff --git a/SodaLive/Sources/MyPage/Profile/ProfileUpdateView.swift b/SodaLive/Sources/MyPage/Profile/ProfileUpdateView.swift new file mode 100644 index 0000000..5fced00 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/ProfileUpdateView.swift @@ -0,0 +1,430 @@ +// +// ProfileUpdateView.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import SwiftUI +import Kingfisher + +struct ProfileUpdateView: View { + @StateObject var viewModel = ProfileUpdateViewModel() + @StateObject var keyboardHandler = KeyboardHandler() + + @State private var isShowPhotoPicker = false + @State private var isShowSelectTagView = false + + let refresh: () -> Void + + @ViewBuilder + func EmailAndPasswordView() -> some View { + VStack(spacing: 26.7) { + VStack(alignment: .leading, spacing: 0) { + Text("이메일") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.leading, 6.7) + + Text(viewModel.email) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + .padding(.top, 12) + .padding(.leading, 6.7) + + Divider() + .frame(height: 0.3) + .foregroundColor(Color(hex: "909090")) + .padding(.top, 8.3) + } + + HStack(alignment: .bottom, spacing: 13.3) { + VStack(alignment: .leading, spacing: 0) { + Text("비밀번호") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.leading, 6.7) + + Text("********") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + .padding(.top, 12) + .padding(.leading, 6.7) + + Divider() + .frame(height: 0.3) + .foregroundColor(Color(hex: "909090")) + .padding(.top, 8.3) + } + + Button(action: { AppState.shared.setAppStep(step: .modifyPassword) }) { + Text("비밀번호 변경") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.white) + .padding(.vertical, 13.3) + .padding(.horizontal, 22.7) + .background(Color(hex: "9970ff")) + .cornerRadius(8) + } + } + } + .padding(.vertical, 20) + .padding(.horizontal, 13.3) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + + @ViewBuilder + func NicknameAndGenderView() -> some View { + VStack(spacing: 16.7) { + HStack(alignment: .bottom, spacing: 13.3) { + VStack(alignment: .leading, spacing: 0) { + Text("닉네임") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.leading, 6.7) + + Text(viewModel.nickname) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.top, 12) + .padding(.leading, 6.7) + + Divider() + .frame(height: 0.3) + .foregroundColor(Color(hex: "909090")) + .padding(.top, 8.3) + } + + Button(action: { AppState.shared.setAppStep(step: .changeNickname) }) { + Text("닉네임 변경") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color.white) + .padding(.vertical, 13.3) + .padding(.horizontal, 22.7) + .background(Color(hex: "9970ff")) + .cornerRadius(8) + } + } + + VStack(alignment: .leading, spacing: 13.3) { + Text("성별") + .font(.custom(Font.bold.rawValue, size: 12)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.leading, 6.7) + + HStack(spacing: 0) { + Button(action: { viewModel.gender = .FEMALE }) { + HStack(spacing: 13.3) { + Image(viewModel.gender == .FEMALE ? "btn_radio_select_selected" : "btn_radio_select_normal") + .resizable() + .frame(width: 20, height: 20) + + Text("여자") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + } + } + + Spacer() + + Button(action: { viewModel.gender = .MALE }) { + HStack(spacing: 13.3) { + Image(viewModel.gender == .MALE ? "btn_radio_select_selected" : "btn_radio_select_normal") + .resizable() + .frame(width: 20, height: 20) + + Text("남자") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + } + } + + Spacer() + + Button(action: { viewModel.gender = .NONE }) { + HStack(spacing: 13.3) { + Image(viewModel.gender == .NONE ? "btn_radio_select_selected" : "btn_radio_select_normal") + .resizable() + .frame(width: 20, height: 20) + + Text("공개 안 함") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + } + } + + Spacer() + } + .padding(.horizontal, 6.7) + } + } + .padding(.vertical, 20) + .padding(.horizontal, 13.3) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + + @ViewBuilder + func InstagramAndYoutubeAccountView() -> some View { + VStack(spacing: 16.7) { + UserTextField( + title: "인스타그램", + hint: "인스타그램 URL", + isSecure: false, + variable: $viewModel.instagramUrl + ) + + UserTextField( + title: "유튜브", + hint: "유튜브 URL", + isSecure: false, + variable: $viewModel.youtubeUrl + ) + + UserTextField( + title: "웹사이트", + hint: "웹사이트 URL", + isSecure: false, + variable: $viewModel.websiteUrl + ) + + UserTextField( + title: "블로그", + hint: "블로그 URL", + isSecure: false, + variable: $viewModel.blogUrl + ) + } + .padding(.vertical, 20) + .padding(.horizontal, 13.3) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + + @ViewBuilder + func TagSelectView() -> some View { + VStack(alignment: .leading, spacing: 13.3) { + Text("관심사") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Button(action: { + hideKeyboard() + isShowSelectTagView = true + }) { + Text("관심사 선택") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 13.7) + .frame(width: screenSize().width - 53.4) + .background(Color(hex: "9970ff").opacity(0.2)) + .cornerRadius(24.3) + .overlay( + RoundedRectangle(cornerRadius: 24.3) + .stroke() + .foregroundColor(Color(hex: "9970ff")) + ) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + ForEach(viewModel.tags, id: \.self) { tag in + HStack(spacing: 6.7) { + Text(tag) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(.white) + + Image("ic_circle_x") + .onTapGesture { + if let index = viewModel.tags.firstIndex(of: tag) { + viewModel.tags.remove(at: index) + } + } + } + .padding(10) + .background(Color(hex: "9970ff")) + .cornerRadius(24.3) + } + } + } + .padding(.top, 13.3) + } + .padding(.vertical, 20) + .padding(.horizontal, 13.3) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + + @ViewBuilder + func ContentInputView() -> some View { + VStack(alignment: .leading, spacing: 13.3) { + Text("소개글") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + TextViewWrapper( + text: $viewModel.introduce, + placeholder: viewModel.placeholder, + textColorHex: "eeeeee", + backgroundColorHex: "222222" + ) + .frame(width: screenSize().width - 26.7, height: 133.3) + .padding(.top, 13.3) + } + .frame(width: screenSize().width - 26.7) + } + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + ZStack { + VStack(spacing: 0) { + DetailNavigationBar(title: "프로필 수정") + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + if let profileResponse = viewModel.profileResponse { + ZStack { + if profileResponse.profileUrl.trimmingCharacters(in: .whitespaces).count > 0 { + KFImage(URL(string: profileResponse.profileUrl)) + .resizable() + .scaledToFill() + .frame(width: 80, height: 80, alignment: .top) + .clipShape(Circle()) + } else { + Image("ic_logo") + .resizable() + .scaledToFill() + .frame(width: 80, height: 80, alignment: .top) + .background(Color(hex: "3e3358")) + .clipShape(Circle()) + } + + Image("ic_camera") + .padding(10) + .background(Color(hex: "9970ff")) + .cornerRadius(30) + .offset(x: 25, y: 25) + } + .frame(alignment: .bottomTrailing) + .padding(.top, 13.3) + .onTapGesture { + isShowPhotoPicker = true + } + + EmailAndPasswordView() + .padding(.top, 26.7) + + NicknameAndGenderView() + .padding(.top, 13.3) + + InstagramAndYoutubeAccountView() + .padding(.top, 13.3) + + TagSelectView() + .padding(.top, 33.3) + + ContentInputView() + .padding(.top, 33.3) + + Text("저장하기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.white) + .frame(width: screenSize().width - 26.7, height: 50) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(.vertical, 13.7) + .padding(.horizontal, 13.3) + .frame(width: screenSize().width) + .background(Color(hex: "222222")) + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .padding(.top, 26.7) + .onTapGesture { + viewModel.updateProfile() + } + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: proxy.size.width, height: 15.3) + } + } + } + } + .padding(.top, 13.3) + } + + if isShowPhotoPicker { + ImagePicker( + isShowing: $isShowPhotoPicker, + selectedImage: $viewModel.profileImage, + sourceType: .photoLibrary + ) + } + + if isShowSelectTagView { + GeometryReader { proxy in + VStack { + Spacer() + MemberTagView( + isShowing: $isShowSelectTagView, + selectedTags: $viewModel.tags, + onItemClick: { tag, isChecked in + if isChecked { + viewModel.addTag(tag: tag) + } else { + viewModel.removeTag(tag: tag) + } + } + ) + .frame(width: proxy.size.width, height: proxy.size.height * 0.9) + .offset(y: isShowSelectTagView ? 0 : proxy.size.height * 0.9) + .animation(.easeInOut(duration: 0.49), value: self.isShowSelectTagView) + } + } + .edgesIgnoringSafeArea(.bottom) + } + } + .offset(y: keyboardHandler.keyboardHeight > 0 ? -keyboardHandler.keyboardHeight + 15.3 : 0) + .edgesIgnoringSafeArea(.bottom) + .onTapGesture { + hideKeyboard() + } + } + .onAppear { + viewModel.refresh = refresh + viewModel.getMyProfile() + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .padding(.horizontal, 6.7) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + } + } +} + +struct ProfileUpdateView_Previews: PreviewProvider { + static var previews: some View { + ProfileUpdateView(refresh: {}) + } +} diff --git a/SodaLive/Sources/MyPage/Profile/ProfileUpdateViewModel.swift b/SodaLive/Sources/MyPage/Profile/ProfileUpdateViewModel.swift new file mode 100644 index 0000000..da7f7e8 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/ProfileUpdateViewModel.swift @@ -0,0 +1,329 @@ +// +// ProfileUpdateViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import UIKit +import Combine + +import Moya + +final class ProfileUpdateViewModel: ObservableObject { + + private let repository = UserRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + @Published var email = "" + @Published var nickname = "" + @Published var youtubeUrl = "" + @Published var instagramUrl = "" + @Published var websiteUrl = "" + @Published var blogUrl = "" + @Published var gender: Gender = .NONE + @Published var introduce = "" + + @Published var currentPassword = "" + @Published var newPassword = "" + @Published var newPasswordConfirm = "" + + @Published var tags = [String]() + @Published var insertTags = [String]() + @Published var removeTags = [String]() + + var profileImage: UIImage? = nil { + didSet { + if let _ = profileImage { + updateProfileImage() + } + } + } + + var profileResponse: GetProfileResponse? + + var refresh: () -> Void = {} + + let placeholder = "소개글을 입력하세요" + + func getMyProfile() { + isLoading = true + repository.getMyProfile() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.profileResponse = data + + self.email = data.email + self.nickname = data.nickname + self.youtubeUrl = data.youtubeUrl ?? "" + self.instagramUrl = data.instagramUrl ?? "" + self.blogUrl = data.blogUrl ?? "" + self.websiteUrl = data.websiteUrl ?? "" + self.introduce = data.introduce + self.gender = data.gender + self.tags.append(contentsOf: data.tags) + + UserDefaults.set(data.nickname, forKey: .nickname) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + + func updateProfile() { + if profileResponse!.nickname != nickname || + profileResponse!.youtubeUrl != youtubeUrl || + profileResponse!.instagramUrl != instagramUrl || + profileResponse!.blogUrl != blogUrl || + profileResponse!.websiteUrl != websiteUrl || + profileResponse!.gender != gender || + profileResponse!.introduce != introduce || + !insertTags.isEmpty || + !removeTags.isEmpty { + + let request = ProfileUpdateRequest( + email: profileResponse!.email, + nickname: profileResponse!.nickname != nickname ? nickname : nil, + gender: profileResponse!.gender != gender ? gender : nil, + introduce: profileResponse!.introduce != introduce && introduce.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? introduce : nil, + youtubeUrl: profileResponse!.youtubeUrl != youtubeUrl ? youtubeUrl : nil, + instagramUrl: profileResponse!.instagramUrl != instagramUrl ? instagramUrl : nil, + websiteUrl: profileResponse!.websiteUrl != websiteUrl ? websiteUrl : nil, + blogUrl: profileResponse!.blogUrl != blogUrl ? blogUrl : nil, + insertTags: !insertTags.isEmpty ? insertTags : nil, + removeTags: !removeTags.isEmpty ? removeTags : nil + ) + + isLoading = true + repository.profileUpdate(request: request) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let _ = decoded.data, decoded.success { + self.refresh() + self.errorMessage = "프로필이 변경되었습니다." + self.isShowPopup = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AppState.shared.back() + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + + } else { + AppState.shared.back() + } + } + + func updatePassword() { + if currentPassword.trimmingCharacters(in: .whitespaces).isEmpty { + errorMessage = "현재 비밀번호를 입력하세요." + isShowPopup = true + return + } + + if newPassword.trimmingCharacters(in: .whitespaces).isEmpty { + errorMessage = "변경할 비밀번호를 입력하세요." + isShowPopup = true + return + } + + if newPassword != newPasswordConfirm { + errorMessage = "비밀번호가 일치하지 않습니다." + isShowPopup = true + return + } + + if !validatePassword() { + errorMessage = "영문, 숫자 포함 8자 이상의 비밀번호를 입력해 주세요." + isShowPopup = true + return + } + + let request = ProfileUpdateRequest( + email: UserDefaults.string(forKey: .email), + password: currentPassword, + modifyPassword: newPassword + ) + + isLoading = true + repository.profileUpdate(request: request) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let _ = decoded.data, decoded.success { + self.errorMessage = "비밀번호가 변경되었습니다." + self.isShowPopup = true + self.currentPassword = "" + self.newPassword = "" + self.newPasswordConfirm = "" + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AppState.shared.back() + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + + func removeTag(tag: String) { + if let index = tags.firstIndex(of: tag) { + tags.remove(at: index) + } + + if (insertTags.contains(tag)) { + if let index = insertTags.firstIndex(of: tag) { + insertTags.remove(at: index) + } + } else { + removeTags.append(tag) + } + } + + func addTag(tag: String) { + tags.append(tag) + if (removeTags.contains(tag)) { + if let index = removeTags.firstIndex(of: tag) { + removeTags.remove(at: index) + } + } else { + insertTags.append(tag) + } + } + + private func updateProfileImage() { + isLoading = false + + if let profileImage = profileImage, let imageData = profileImage.jpegData(compressionQuality: 0.8) { + repository + .profileImageUpdate(parameter: MultipartFormData( + provider: .data(imageData), + name: "image", + fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", + mimeType: "image/*" + )) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let _ = decoded.data, decoded.success { + self.refresh() + self.getMyProfile() + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } else { + self.errorMessage = "프로필 이미지를 업데이트 하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + self.isLoading = false + } + } + + private func validatePassword() -> Bool { + let passwordRegEx = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d$@!%*#?&]{8,}$" + + let predicate = NSPredicate(format:"SELF MATCHES %@", passwordRegEx) + return predicate.evaluate(with: newPassword) + } +} diff --git a/SodaLive/Sources/MyPage/Profile/Tag/MemberTagApi.swift b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagApi.swift new file mode 100644 index 0000000..51d5025 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagApi.swift @@ -0,0 +1,44 @@ +// +// MemberTagApi.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation +import Moya + +enum MemberTagApi { + case getTags +} + +extension MemberTagApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .getTags: + return "/member/tag" + } + } + + var method: Moya.Method { + switch self { + case .getTags: + return .get + } + } + + var task: Task { + switch self { + case .getTags: + return .requestPlain + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/MyPage/Profile/Tag/MemberTagRepository.swift b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagRepository.swift new file mode 100644 index 0000000..d3ac1b5 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagRepository.swift @@ -0,0 +1,19 @@ +// +// MemberTagRepository.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class MemberTagRepository { + private let api = MoyaProvider() + + func getTags() -> AnyPublisher { + return api.requestPublisher(.getTags) + } +} diff --git a/SodaLive/Sources/MyPage/Profile/Tag/MemberTagResponse.swift b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagResponse.swift new file mode 100644 index 0000000..1d23d7a --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagResponse.swift @@ -0,0 +1,14 @@ +// +// MemberTagResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation + +struct MemberTagResponse: Decodable, Hashable { + let id: Int + let tag: String + let image: String +} diff --git a/SodaLive/Sources/MyPage/Profile/Tag/MemberTagView.swift b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagView.swift new file mode 100644 index 0000000..70794d5 --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagView.swift @@ -0,0 +1,115 @@ +// +// MemberTagView.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import SwiftUI +import Kingfisher + +struct MemberTagView: View { + + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + + @StateObject var viewModel = MemberTagViewModel() + + @Binding var isShowing: Bool + @Binding var selectedTags: [String] + let onItemClick: (String, Bool) -> Void + + var body: some View { + ZStack { + Color(hex: "222222").ignoresSafeArea() + + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 0) { + Text("관심사 선택") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + + Spacer() + + Image("ic_close_white") + .resizable() + .frame(width: 20, height: 20) + .onTapGesture { + isShowing = false + } + } + .padding(.horizontal, 26.7) + .padding(.top, 26.7) + + ScrollView(.vertical, showsIndicators: false) { + LazyVGrid(columns: columns, spacing: 26.7) { + ForEach(viewModel.tags, id: \.self) { tag in + VStack(spacing: 16.7) { + ZStack { + KFImage(URL(string: tag.image)) + .resizable() + .scaledToFill() + .frame(width: 60, height: 60, alignment: .top) + .clipped() + + if selectedTags.contains(tag.tag) { + Image("ic_tag_check") + .resizable() + .frame(width: 60, height: 60) + } + } + + Text(tag.tag) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor( + selectedTags.contains(tag.tag) ? + Color(hex: "9970ff") : + Color(hex: "bbbbbb") + ) + } + .onTapGesture { + onItemClick(tag.tag, !selectedTags.contains(tag.tag)) + } + } + } + } + .padding(.horizontal, 20) + .padding(.top, 26.7) + + Text("확인") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.vertical, 16) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(.bottom, 26.7) + .onTapGesture { + isShowing = false + } + } + + if viewModel.isLoading { + LoadingView() + } + } + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .onAppear { + viewModel.getTags() + } + } +} + +struct MemberTagView_Previews: PreviewProvider { + static var previews: some View { + MemberTagView( + isShowing: .constant(true), + selectedTags: .constant(["여행"]), + onItemClick: { _, _ in } + ) + } +} diff --git a/SodaLive/Sources/MyPage/Profile/Tag/MemberTagViewModel.swift b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagViewModel.swift new file mode 100644 index 0000000..3af589a --- /dev/null +++ b/SodaLive/Sources/MyPage/Profile/Tag/MemberTagViewModel.swift @@ -0,0 +1,59 @@ +// +// MemberTagViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/19. +// + +import Foundation +import Combine + +final class MemberTagViewModel: ObservableObject { + + private let repository = MemberTagRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var tags: [MemberTagResponse] = [] + + func getTags() { + isLoading = true + + repository.getTags() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[MemberTagResponse]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.tags.removeAll() + self.tags.append(contentsOf: data) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } +} diff --git a/SodaLive/Sources/User/UserApi.swift b/SodaLive/Sources/User/UserApi.swift index 670af70..252540c 100644 --- a/SodaLive/Sources/User/UserApi.swift +++ b/SodaLive/Sources/User/UserApi.swift @@ -24,6 +24,12 @@ enum UserApi { case creatorUnFollow(request: CreatorFollowRequest) case memberBlock(request: MemberBlockRequest) case memberUnBlock(request: MemberBlockRequest) + case getMyProfile + case profileImageUpdate(parameter: MultipartFormData) + case profileUpdate(request: ProfileUpdateRequest) + case getChangeNicknamePrice + case checkNickname(nickname: String) + case changeNickname(request: ProfileUpdateRequest) } extension UserApi: TargetType { @@ -77,18 +83,34 @@ extension UserApi: TargetType { case .memberUnBlock: return "/member/unblock" + + case .getMyProfile, .profileUpdate: + return "/member" + + case .profileImageUpdate: + return "/member/image" + + case .getChangeNicknamePrice: + return "/member/change/nickname/price" + + case .checkNickname: + return "/member/check/nickname" + + case .changeNickname: + return "/member/change/nickname" } } var method: Moya.Method { switch self { - case .login, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut, .creatorFollow, .creatorUnFollow, .memberBlock, .memberUnBlock: + case .login, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut, .creatorFollow, .creatorUnFollow, .memberBlock, .memberUnBlock, + .profileImageUpdate: return .post - case .searchUser, .getMypage, .getMemberInfo: + case .searchUser, .getMypage, .getMemberInfo, .getMyProfile, .getChangeNicknamePrice, .checkNickname: return .get - case .updatePushToken: + case .updatePushToken, .profileUpdate, .changeNickname: return .put } } @@ -107,10 +129,10 @@ extension UserApi: TargetType { case .searchUser(let nickname): return .requestParameters(parameters: ["nickname" : nickname], encoding: URLEncoding.queryString) - case .getMypage: + case .getMypage, .getMyProfile: return .requestParameters(parameters: ["container" : "ios"], encoding: URLEncoding.queryString) - case .getMemberInfo, .logout, .logoutAllDevice: + case .getMemberInfo, .logout, .logoutAllDevice, .getChangeNicknamePrice: return .requestPlain case .notification(let request): @@ -133,6 +155,18 @@ extension UserApi: TargetType { case .memberUnBlock(let request): return .requestJSONEncodable(request) + + case .profileImageUpdate(let parameter): + return .uploadMultipart([parameter]) + + case .profileUpdate(let request): + return .requestJSONEncodable(request) + + case .checkNickname(let nickname): + return .requestParameters(parameters: ["nickname" : nickname], encoding: URLEncoding.queryString) + + case .changeNickname(let request): + return .requestJSONEncodable(request) } } diff --git a/SodaLive/Sources/User/UserRepository.swift b/SodaLive/Sources/User/UserRepository.swift index 7c2d8b0..a5855ff 100644 --- a/SodaLive/Sources/User/UserRepository.swift +++ b/SodaLive/Sources/User/UserRepository.swift @@ -84,4 +84,28 @@ final class UserRepository { func memberUnBlock(userId: Int) -> AnyPublisher { return api.requestPublisher(.memberUnBlock(request: MemberBlockRequest(blockMemberId: userId))) } + + func getMyProfile() -> AnyPublisher { + return api.requestPublisher(.getMyProfile) + } + + func profileImageUpdate(parameter: MultipartFormData) -> AnyPublisher { + return api.requestPublisher(.profileImageUpdate(parameter: parameter)) + } + + func profileUpdate(request: ProfileUpdateRequest) -> AnyPublisher { + return api.requestPublisher(.profileUpdate(request: request)) + } + + func getChangeNicknamePrice() -> AnyPublisher { + return api.requestPublisher(.getChangeNicknamePrice) + } + + func checkNickname(nickname: String) -> AnyPublisher { + return api.requestPublisher(.checkNickname(nickname: nickname)) + } + + func changeNickname(request: ProfileUpdateRequest) -> AnyPublisher { + return api.requestPublisher(.changeNickname(request: request)) + } }