diff --git a/Projects/Core/Sources/Storage/UserDefault/UserDefaultStorage.swift b/Projects/Core/Sources/Storage/UserDefault/UserDefaultStorage.swift index a405f33..94e7458 100644 --- a/Projects/Core/Sources/Storage/UserDefault/UserDefaultStorage.swift +++ b/Projects/Core/Sources/Storage/UserDefault/UserDefaultStorage.swift @@ -6,6 +6,7 @@ public enum UserDefaultKeys: String { case pickerTimeMode case userInfoData case userNameData + case userProfileImageData } public struct UserDefaultStorage: UserDefault { diff --git a/Projects/Data/Sources/API/AuthAPI.swift b/Projects/Data/Sources/API/AuthAPI.swift index 2f8f8f6..97c8f49 100644 --- a/Projects/Data/Sources/API/AuthAPI.swift +++ b/Projects/Data/Sources/API/AuthAPI.swift @@ -15,7 +15,7 @@ extension AuthAPI: PiCKAPI { public typealias ErrorType = PiCKError public var urlType: PiCKURL { - .user + return .user } public var urlPath: String { diff --git a/Projects/Data/Sources/API/BugAPI.swift b/Projects/Data/Sources/API/BugAPI.swift index 0ac12e2..6c718ba 100644 --- a/Projects/Data/Sources/API/BugAPI.swift +++ b/Projects/Data/Sources/API/BugAPI.swift @@ -15,7 +15,7 @@ extension BugAPI: PiCKAPI { public typealias ErrorType = PiCKError public var urlType: PiCKURL { - .bug + return .bug } public var urlPath: String { diff --git a/Projects/Data/Sources/API/ProfileAPI.swift b/Projects/Data/Sources/API/ProfileAPI.swift index 35d787f..d60cfc3 100644 --- a/Projects/Data/Sources/API/ProfileAPI.swift +++ b/Projects/Data/Sources/API/ProfileAPI.swift @@ -9,6 +9,7 @@ import AppNetwork public enum ProfileAPI { case fetchSimpleProfile case fetchDetailProfile + case uploadProfileImage(image: Data) } extension ProfileAPI: PiCKAPI { @@ -24,15 +25,35 @@ extension ProfileAPI: PiCKAPI { return "/simple" case .fetchDetailProfile: return "/details" + case .uploadProfileImage: + return "/profile" } } public var method: Moya.Method { - return .get + switch self { + case .uploadProfileImage: + return .patch + default: + return .get + } } public var task: Moya.Task { - return .requestPlain + switch self { + case .uploadProfileImage(let image): + var multiformData: [MultipartFormData] = [] + multiformData.append(.init( + provider: .data(image), + name: "file", + fileName: "file.jpg", + mimeType: "file/jpg" + )) + + return .uploadMultipart(multiformData) + default: + return .requestPlain + } } public var pickHeader: TokenType { diff --git a/Projects/Data/Sources/DI/UseCaseAssembly.swift b/Projects/Data/Sources/DI/UseCaseAssembly.swift index f1f8965..424d7c4 100644 --- a/Projects/Data/Sources/DI/UseCaseAssembly.swift +++ b/Projects/Data/Sources/DI/UseCaseAssembly.swift @@ -77,6 +77,9 @@ public final class UseCaseAssembly: Assembly { container.register(FetchDetailProfileUseCase.self) { resolver in FetchDetailProfileUseCase(repository: resolver.resolve(ProfileRepository.self)!) } + container.register(UploadProfileImageUseCase.self) { resolver in + UploadProfileImageUseCase(repository: resolver.resolve(ProfileRepository.self)!) + } // bug container.register(BugImageUploadUseCase.self) { resolver in BugImageUploadUseCase(repository: resolver.resolve(BugRepository.self)!) diff --git a/Projects/Data/Sources/DataSource/ProfileDataSource.swift b/Projects/Data/Sources/DataSource/ProfileDataSource.swift index d94228a..adce27c 100644 --- a/Projects/Data/Sources/DataSource/ProfileDataSource.swift +++ b/Projects/Data/Sources/DataSource/ProfileDataSource.swift @@ -10,6 +10,7 @@ import Domain protocol ProfileDataSource { func fetchSimpleProfile() -> Single func fetchDetailProfile() -> Single + func uploadProfileImage(image: Data) -> Completable } class ProfileDataSourceImpl: BaseDataSource, ProfileDataSource { @@ -23,4 +24,10 @@ class ProfileDataSourceImpl: BaseDataSource, ProfileDataSource { .filterSuccessfulStatusCodes() } + func uploadProfileImage(image: Data) -> Completable { + return request(.uploadProfileImage(image: image)) + .filterSuccessfulStatusCodes() + .asCompletable() + } + } diff --git a/Projects/Data/Sources/Repository/ProfileRepositoryImpl.swift b/Projects/Data/Sources/Repository/ProfileRepositoryImpl.swift index 4d2f2a3..cebac91 100644 --- a/Projects/Data/Sources/Repository/ProfileRepositoryImpl.swift +++ b/Projects/Data/Sources/Repository/ProfileRepositoryImpl.swift @@ -23,4 +23,8 @@ class ProfileRepositoryImpl: ProfileRepository { .map { $0.toDomain() } } + func uploadProfileImage(image: Data) -> Completable { + return remoteDataSource.uploadProfileImage(image: image) + } + } diff --git a/Projects/Domain/Sources/Repository/ProfileRepository.swift b/Projects/Domain/Sources/Repository/ProfileRepository.swift index 254e401..6a1f796 100644 --- a/Projects/Domain/Sources/Repository/ProfileRepository.swift +++ b/Projects/Domain/Sources/Repository/ProfileRepository.swift @@ -5,4 +5,5 @@ import RxSwift public protocol ProfileRepository { func fetchSimpleProfile() -> Single func fetchDetailProfile() -> Single + func uploadProfileImage(image: Data) -> Completable } diff --git a/Projects/Domain/Sources/UseCase/Profile/UploadProfileImageUseCase.swift b/Projects/Domain/Sources/UseCase/Profile/UploadProfileImageUseCase.swift new file mode 100644 index 0000000..2627394 --- /dev/null +++ b/Projects/Domain/Sources/UseCase/Profile/UploadProfileImageUseCase.swift @@ -0,0 +1,16 @@ +import Foundation + +import RxSwift + +public class UploadProfileImageUseCase { + let repository: ProfileRepository + + public init(repository: ProfileRepository) { + self.repository = repository + } + + public func execute(image: Data) -> Completable { + return repository.uploadProfileImage(image: image) + } + +} diff --git a/Projects/Modules/DesignSystem/Sources/Component/View/PiCKProfileView.swift b/Projects/Modules/DesignSystem/Sources/Component/View/PiCKProfileView.swift index e278ca1..63a3d55 100644 --- a/Projects/Modules/DesignSystem/Sources/Component/View/PiCKProfileView.swift +++ b/Projects/Modules/DesignSystem/Sources/Component/View/PiCKProfileView.swift @@ -7,6 +7,8 @@ import RxSwift import RxCocoa import RxGesture +import Kingfisher + import Core public class PiCKProfileView: BaseView { @@ -18,10 +20,13 @@ public class PiCKProfileView: BaseView { ) public func setup( - image: UIImage, + image: String, info: String ) { - self.profileImageView.image = image + self.profileImageView.kf.setImage( + with: URL(string: image), + placeholder: UIImage.profile + ) self.userInfoLabel.text = "대덕소프트웨어마이스터고\n\(info)" } diff --git a/Projects/Presentation/Sources/DI/PresentationAssembly.swift b/Projects/Presentation/Sources/DI/PresentationAssembly.swift index 710012d..695da57 100644 --- a/Projects/Presentation/Sources/DI/PresentationAssembly.swift +++ b/Projects/Presentation/Sources/DI/PresentationAssembly.swift @@ -182,7 +182,8 @@ public final class PresentationAssembly: Assembly { } container.register(MyPageViewModel.self) { resolver in MyPageViewModel( - profileUsecase: resolver.resolve(FetchDetailProfileUseCase.self)! + profileUsecase: resolver.resolve(FetchDetailProfileUseCase.self)!, + uploadProfileImageUseCase: resolver.resolve(UploadProfileImageUseCase.self)! ) } } diff --git a/Projects/Presentation/Sources/Scene/AllTab/AllTabViewController.swift b/Projects/Presentation/Sources/Scene/AllTab/AllTabViewController.swift index 11b2154..3c5d86d 100644 --- a/Projects/Presentation/Sources/Scene/AllTab/AllTabViewController.swift +++ b/Projects/Presentation/Sources/Scene/AllTab/AllTabViewController.swift @@ -104,8 +104,15 @@ public class AllTabViewController: BaseViewController { } public override func setLayoutData() { - let userInfoData = UserDefaultStorage.shared.get(forKey: .userInfoData) as? String - self.profileView.setup(image: .profile, info: userInfoData ?? "정보가 없는 사용자") + let userDefaultStorage = UserDefaultStorage.shared + + let userInfoData = userDefaultStorage.get(forKey: .userInfoData) as? String + let userInfoImage = userDefaultStorage.get(forKey: .userProfileImageData) as? String + + self.profileView.setup( + image: userInfoImage ?? "", + info: userInfoData ?? "정보가 없는 사용자" + ) } } diff --git a/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewController.swift b/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewController.swift index 8586a45..9c043a8 100644 --- a/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewController.swift +++ b/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewController.swift @@ -2,17 +2,25 @@ import UIKit import SnapKit import Then + import RxSwift import RxCocoa +import Kingfisher + import Core import DesignSystem public class MyPageViewController: BaseViewController { - private let profileImageView = UIImageView(image: .profile) + private var profileImageData = PublishRelay() + + private let profileImageView = UIImageView().then { + $0.contentMode = .scaleToFill + $0.layer.cornerRadius = 40 + $0.clipsToBounds = true + } private let changeButton = UIButton(type: .system).then { -// $0.setTitle("변경하기", for: .normal) - $0.setTitle("", for: .normal) + $0.setTitle("변경하기", for: .normal) $0.setTitleColor(.gray500, for: .normal) $0.titleLabel?.font = .body1 } @@ -47,17 +55,33 @@ public class MyPageViewController: BaseViewController { } public override func bind() { let input = MyPageViewModel.Input( - viewWillAppear: viewWillAppearRelay.asObservable() + viewWillAppear: viewWillAppearRelay.asObservable(), + profileImage: profileImageData.asObservable() ) let output = viewModel.transform(input: input) output.profileData.asObservable() - .bind(onNext: { [weak self] profileData in - self?.userNameLabel.text = profileData.name - self?.userBirthDayLabel.text = "\(profileData.birthDay.toDate(type: .fullDate).toString(type: .fullDateKorForCalendar))" - self?.userSchoolIDLabel.text = "\(profileData.grade)학년 \(profileData.classNum)반 \(profileData.num)번" - self?.userIDLabel.text = profileData.accountID - }).disposed(by: disposeBag) + .withUnretained(self) + .bind { owner, profileData in + owner.profileImageView.kf.setImage( + with: URL(string: profileData.profile ?? ""), + placeholder: UIImage.profile + ) + owner.userNameLabel.text = profileData.name + owner.userBirthDayLabel.text = "\(profileData.birthDay.toDate(type: .fullDate).toString(type: .fullDateKorForCalendar))" + owner.userSchoolIDLabel.text = "\(profileData.grade)학년 \(profileData.classNum)반 \(profileData.num)번" + owner.userIDLabel.text = profileData.accountID + }.disposed(by: disposeBag) + } + public override func bindAction() { + changeButton.rx.tap + .bind { + let picker = UIImagePickerController() + picker.sourceType = .photoLibrary + picker.allowsEditing = true + picker.delegate = self + self.present(picker, animated: true) + }.disposed(by: disposeBag) } public override func addView() { @@ -72,6 +96,7 @@ public class MyPageViewController: BaseViewController { profileImageView.snp.makeConstraints { $0.centerX.equalToSuperview() $0.top.equalTo(view.safeAreaLayoutGuide).offset(40) + $0.size.equalTo(80) } changeButton.snp.makeConstraints { $0.centerX.equalToSuperview() @@ -99,3 +124,16 @@ public class MyPageViewController: BaseViewController { } } + +extension MyPageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + public func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] + ) { + picker.dismiss(animated: true) { + let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage + self.profileImageView.image = image + self.profileImageData.accept(info[UIImagePickerController.InfoKey.editedImage] as? Data ?? Data()) + } + } +} diff --git a/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewModel.swift b/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewModel.swift index 40332c6..29afa3a 100644 --- a/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewModel.swift +++ b/Projects/Presentation/Sources/Scene/AllTab/MyPage/MyPageViewModel.swift @@ -12,13 +12,19 @@ public class MyPageViewModel: BaseViewModel, Stepper { public var steps = PublishRelay() private let profileUsecase: FetchDetailProfileUseCase + private let uploadProfileImageUseCase: UploadProfileImageUseCase - public init(profileUsecase: FetchDetailProfileUseCase) { + public init( + profileUsecase: FetchDetailProfileUseCase, + uploadProfileImageUseCase: UploadProfileImageUseCase + ) { self.profileUsecase = profileUsecase + self.uploadProfileImageUseCase = uploadProfileImageUseCase } public struct Input { let viewWillAppear: Observable + let profileImage: Observable } public struct Output { let profileData: Signal @@ -38,6 +44,17 @@ public class MyPageViewModel: BaseViewModel, Stepper { .bind(to: profileData) .disposed(by: disposeBag) + input.profileImage + .flatMap { image in + self.uploadProfileImageUseCase.execute(image: image) + .catch { + print($0.localizedDescription) + return .never() + } + } + .subscribe() + .disposed(by: disposeBag) + return Output(profileData: profileData.asSignal()) } diff --git a/Projects/Presentation/Sources/Scene/Home/HomeViewController.swift b/Projects/Presentation/Sources/Scene/Home/HomeViewController.swift index 89a173f..d0e4053 100644 --- a/Projects/Presentation/Sources/Scene/Home/HomeViewController.swift +++ b/Projects/Presentation/Sources/Scene/Home/HomeViewController.swift @@ -297,8 +297,15 @@ public class HomeViewController: BaseViewController { } public override func setLayoutData() { - let userInfoData = UserDefaultStorage.shared.get(forKey: .userInfoData) as? String - self.profileView.setup(image: .profile, info: userInfoData ?? "정보가 없는 사용자") + let userDefaultStorage = UserDefaultStorage.shared + + let userInfoData = userDefaultStorage.get(forKey: .userInfoData) as? String + let userInfoImage = userDefaultStorage.get(forKey: .userProfileImageData) as? String + + self.profileView.setup( + image: userInfoImage ?? "", + info: userInfoData ?? "정보가 없는 사용자" + ) } private func setupViewType(type: HomeViewType) { diff --git a/Projects/Presentation/Sources/Scene/Home/HomeViewModel.swift b/Projects/Presentation/Sources/Scene/Home/HomeViewModel.swift index d5cceaf..62f59fb 100644 --- a/Projects/Presentation/Sources/Scene/Home/HomeViewModel.swift +++ b/Projects/Presentation/Sources/Scene/Home/HomeViewModel.swift @@ -113,9 +113,11 @@ public class HomeViewModel: BaseViewModel, Stepper { } } .subscribe(onNext: { data in - let value = "\(data.grade)학년 \(data.classNum)반 \(data.num)번 \(data.name)" - self.userDefaultStorage.set(to: value, forKey: .userInfoData) + let infoValue = "\(data.grade)학년 \(data.classNum)반 \(data.num)번 \(data.name)" + + self.userDefaultStorage.set(to: infoValue, forKey: .userInfoData) self.userDefaultStorage.set(to: data.name, forKey: .userNameData) +// self.userDefaultStorage.set(to: data.profile, forKey: .userProfileImageData) }).disposed(by: disposeBag) input.viewWillAppear