From aeeaf4e4d5ad0b7da229a6a64b935cf4f749e86d Mon Sep 17 00:00:00 2001 From: hooni Date: Wed, 4 Sep 2024 19:22:56 +0900 Subject: [PATCH 01/13] =?UTF-8?q?chore:=20=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum.xcodeproj/project.pbxproj | 2 + KkuMulKum/Resource/Info.plist | 6 +- .../ViewController/MyPageViewController.swift | 95 ++++++++++--------- .../ViewModel/MyPageEditViewModel.swift | 34 +++++-- 4 files changed, 80 insertions(+), 57 deletions(-) diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 76852aa9..981b289b 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -2214,6 +2214,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "꾸물꿈"; INFOPLIST_KEY_NSCameraUsageDescription = "카메라 사용 권한이 필요합니다."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "사진 라이브러리 접근 권한이 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "꾸물꿈은 카메라 권한을 필요로 합니다. 카메라를 통해 자신의 프로필을 즉시 찍어 업로드할 수 있습니다. 허용 안함 시 일부 기능이 동작하지 않을 수 있습니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -2251,6 +2252,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "꾸물꿈"; INFOPLIST_KEY_NSCameraUsageDescription = "카메라 사용 권한이 필요합니다."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "사진 라이브러리 접근 권한이 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "꾸물꿈은 카메라 권한을 필요로 합니다. 카메라를 통해 자신의 프로필을 즉시 찍어 업로드할 수 있습니다. 허용 안함 시 일부 기능이 동작하지 않을 수 있습니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; diff --git a/KkuMulKum/Resource/Info.plist b/KkuMulKum/Resource/Info.plist index d11ad3c0..f635e2b4 100644 --- a/KkuMulKum/Resource/Info.plist +++ b/KkuMulKum/Resource/Info.plist @@ -2,15 +2,15 @@ - NSPhotoLibraryUsageDescription - 꾸물꿈은 카메라 권한을 필요로 합니다. 카메라를 통해 자신의 프로필을 즉시 찍어 업로드할 수 있습니다. 허용 안함 시 일부 기능이 동작하지 않을 수 있습니다. CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLSchemes - + + kakao69aeef4a49d5b6772d62efdf1686994c + FirebaseAppDelegateProxyEnabled diff --git a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift index 9bf42298..7a20602a 100644 --- a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift +++ b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift @@ -138,29 +138,31 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { } private func updateProfileImage(with urlString: String?) { - if let urlString = urlString, let url = URL(string: urlString) { - rootView.contentView.profileImageView.kf.setImage( - with: url, - placeholder: UIImage.imgProfile, - options: [ - .transition(.fade(0.2)), - .forceRefresh, - .cacheOriginalImage - ], - completionHandler: { result in - switch result { - case .success(_): - print("Profile image loaded successfully") - case .failure(let error): - print("Failed to load profile image: \(error.localizedDescription)") - self.rootView.contentView.profileImageView.image = UIImage.imgProfile + print("Attempting to update profile image with URL: \(urlString ?? "nil")") + if let urlString = urlString, let url = URL(string: urlString) { + rootView.contentView.profileImageView.kf.setImage( + with: url, + placeholder: UIImage.imgProfile, + options: [ + .transition(.fade(0.2)), + .forceRefresh, + .cacheOriginalImage + ], + completionHandler: { result in + switch result { + case .success(let value): + print("Profile image loaded successfully. Size: \(value.image.size)") + case .failure(let error): + print("Failed to load profile image: \(error.localizedDescription)") + self.rootView.contentView.profileImageView.image = UIImage.imgProfile + } } - } - ) - } else { - rootView.contentView.profileImageView.image = UIImage.imgProfile + ) + } else { + print("Invalid URL or nil. Setting default profile image.") + rootView.contentView.profileImageView.image = UIImage.imgProfile + } } - } private func loadImage(from urlString: String, into imageView: UIImageView) { guard let url = URL(string: urlString) else { @@ -198,29 +200,34 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { } private func pushEditProfileViewController() { - let authService = AuthService() - let editProfileViewModel = MyPageEditViewModel(authService: authService) - let editProfileViewController = MyPageEditViewController(viewModel: editProfileViewModel) - - editProfileViewModel.profileImageUpdated - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] imageDataString in - if let imageDataString = imageDataString, - let imageData = Data(base64Encoded: imageDataString), - let image = UIImage(data: imageData) { - self?.rootView.contentView.profileImageView.image = image - } else { - self?.rootView.contentView.profileImageView.image = UIImage.imgProfile - } - KingfisherManager.shared.cache.clearMemoryCache() - KingfisherManager.shared.cache.clearDiskCache() - }) - .disposed(by: disposeBag) - - editProfileViewController.hidesBottomBarWhenPushed = true - - navigationController?.pushViewController(editProfileViewController, animated: true) - } + let authService = AuthService() + let editProfileViewModel = MyPageEditViewModel(authService: authService) + let editProfileViewController = MyPageEditViewController(viewModel: editProfileViewModel) + + editProfileViewModel.profileImageUpdated + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] imageDataString in + print("Profile image update received. Data string length: \(imageDataString?.count ?? 0)") + if let imageDataString = imageDataString, + let imageData = Data(base64Encoded: imageDataString), + let image = UIImage(data: imageData) { + print("Successfully created UIImage from updated profile data") + self?.rootView.contentView.profileImageView.image = image + // Force refresh Kingfisher cache + KingfisherManager.shared.cache.removeImage(forKey: self?.viewModel.userInfo.value?.profileImageURL ?? "") + } else { + print("Failed to create UIImage from updated profile data. Setting default image.") + self?.rootView.contentView.profileImageView.image = UIImage.imgProfile + } + // Refresh user info to get the updated profile image URL + self?.viewModel.fetchUserInfo() + }) + .disposed(by: disposeBag) + + editProfileViewController.hidesBottomBarWhenPushed = true + + navigationController?.pushViewController(editProfileViewController, animated: true) + } private func pushAskViewController() { let askViewController = MyPageAskViewController(viewModel: self.viewModel) diff --git a/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift b/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift index 85245200..fc66f8fb 100644 --- a/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift +++ b/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift @@ -40,12 +40,10 @@ class MyPageEditViewModel: ViewModelType { let serverResponseRelay = PublishRelay() input.newProfileImage - .compactMap { $0?.jpegData(compressionQuality: 1.0) } - .bind(to: imageDataRelay) - .disposed(by: disposeBag) - - input.skipButtonTap - .map { _ in UIImage.imgProfile.jpegData(compressionQuality: 1.0) } + .compactMap { $0?.jpegData(compressionQuality: 0.8) } + .do(onNext: { data in + print("New profile image data size: \(data.count) bytes") + }) .bind(to: imageDataRelay) .disposed(by: disposeBag) @@ -64,11 +62,13 @@ class MyPageEditViewModel: ViewModelType { .withLatestFrom(imageDataRelay) .flatMapLatest { [weak self] imageData -> Observable in guard let self = self, let imageData = imageData else { + print("No image data available for upload") return .just("이미지 데이터가 없습니다.") } return Observable.create { observer in Task { do { + print("Attempting to upload image data of size: \(imageData.count) bytes") let _: EmptyModel = try await self.authService.performRequest( .updateProfileImage( image: imageData, @@ -76,11 +76,13 @@ class MyPageEditViewModel: ViewModelType { mimeType: "image/jpeg" ) ) + print("Profile image upload successful") self.profileImageUpdated.onNext(imageData.base64EncodedString()) observer.onNext("프로필 이미지가 성공적으로 업로드되었습니다.") observer.onCompleted() } catch { let networkError = error as? NetworkError ?? .unknownError("알 수 없는 오류가 발생했습니다.") + print("Profile image upload failed: \(networkError)") observer.onNext(self.handleError(networkError)) observer.onCompleted() } @@ -97,8 +99,14 @@ class MyPageEditViewModel: ViewModelType { return Observable.create { observer in Task { do { - let _: EmptyModel = try await self.authService.performRequest(.updateProfileImage(image: UIImage.imgProfile.jpegData(compressionQuality: 1.0)!, fileName: "default_profile.jpg", mimeType: "image/jpeg")) - // 성공 시 profileImageUpdated에 nil 전달 (기본 이미지로 설정됨을 의미) + let defaultImageData = UIImage.imgProfile.jpegData(compressionQuality: 1.0) ?? Data() + let _: EmptyModel = try await self.authService.performRequest( + .updateProfileImage( + image: defaultImageData, + fileName: "default_profile.jpg", + mimeType: "image/jpeg" + ) + ) self.profileImageUpdated.onNext(nil) observer.onNext("프로필 이미지가 기본 이미지로 변경되었습니다.") observer.onCompleted() @@ -143,8 +151,14 @@ class MyPageEditViewModel: ViewModelType { return "네트워크 오류: \(error.localizedDescription)" case .decodingError: return "데이터 처리 중 오류가 발생했습니다." - default: - return "알 수 없는 오류가 발생했습니다." + case .unknownError(let message): + return "알 수 없는 오류가 발생했습니다: \(message)" + case .invalidImageFormat: + return "잘못된 이미지 형식입니다. 지원되는 형식의 이미지를 선택해주세요." + case .imageSizeExceeded: + return "이미지 크기가 허용 한도를 초과했습니다. 더 작은 이미지를 선택해주세요." + case .userNotFound: + return "사용자를 찾을 수 없습니다. 로그인 상태를 확인해주세요." } } } From ebd51039cf9e671caf3b83fb00c3087c17ffcf0c Mon Sep 17 00:00:00 2001 From: hooni Date: Thu, 5 Sep 2024 21:43:54 +0900 Subject: [PATCH 02/13] =?UTF-8?q?fix/#363=20=EC=9D=B4=EC=9A=A9=EC=95=BD?= =?UTF-8?q?=EA=B4=80=20=ED=83=AD=EB=B0=94=EC=9D=B4=EC=8A=88=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPageTermsViewController.swift | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/KkuMulKum/Source/MyPage/ViewController/MyPageTermsViewController.swift b/KkuMulKum/Source/MyPage/ViewController/MyPageTermsViewController.swift index 49c2ad0c..33581797 100644 --- a/KkuMulKum/Source/MyPage/ViewController/MyPageTermsViewController.swift +++ b/KkuMulKum/Source/MyPage/ViewController/MyPageTermsViewController.swift @@ -8,6 +8,8 @@ import UIKit import WebKit +import SnapKit + class MyPageTermsViewController: BaseViewController { private let viewModel: MyPageViewModel @@ -26,21 +28,46 @@ class MyPageTermsViewController: BaseViewController { let webConfiguration = WKWebViewConfiguration() webView = WKWebView(frame: .zero, configuration: webConfiguration) webView.uiDelegate = self - view = webView + view = UIView() + view.addSubview(webView) } override func viewDidLoad() { super.viewDidLoad() - let myURL = URL(string: "https://arrow-frog-4b9.notion.site/a66033a3ff4a40bfaa6eff0a5bee737d") - let myRequest = URLRequest(url: myURL!) - webView.load(myRequest) - } - - override func setupView() { - super.setupView() setupNavigationBarTitle(with: "이용약관") setupNavigationBarBackButton() + + setupConstraints() + + if let myURL = URL(string: "https://arrow-frog-4b9.notion.site/a66033a3ff4a40bfaa6eff0a5bee737d") { + let myRequest = URLRequest(url: myURL) + webView.load(myRequest) + } + } + + private func setupConstraints() { + webView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide.snp.top) + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + adjustWebViewContentInset() + } + + private func adjustWebViewContentInset() { + let contentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: view.safeAreaInsets.bottom + (tabBarController?.tabBar.frame.height ?? 0), + right: 0 + ) + webView.scrollView.contentInset = contentInset + webView.scrollView.scrollIndicatorInsets = contentInset } } From f6a7a1ddfd0f671a0ab2f9d0015ea2878867fce3 Mon Sep 17 00:00:00 2001 From: hooni Date: Thu, 5 Sep 2024 22:53:49 +0900 Subject: [PATCH 03/13] =?UTF-8?q?fix/#363=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPageEditViewController.swift | 8 ++ .../ViewController/MyPageViewController.swift | 134 ++++++++---------- .../ViewModel/MyPageEditViewModel.swift | 27 ++++ 3 files changed, 98 insertions(+), 71 deletions(-) diff --git a/KkuMulKum/Source/MyPage/ViewController/MyPageEditViewController.swift b/KkuMulKum/Source/MyPage/ViewController/MyPageEditViewController.swift index f0822379..e5adcbb1 100644 --- a/KkuMulKum/Source/MyPage/ViewController/MyPageEditViewController.swift +++ b/KkuMulKum/Source/MyPage/ViewController/MyPageEditViewController.swift @@ -16,6 +16,7 @@ class MyPageEditViewController: BaseViewController { private let viewModel: MyPageEditViewModel private let disposeBag = DisposeBag() private let newProfileImageSubject = PublishSubject() + let profileImageUpdated = PublishSubject() init(viewModel: MyPageEditViewModel) { self.viewModel = viewModel @@ -73,6 +74,13 @@ class MyPageEditViewController: BaseViewController { newProfileImage: newProfileImageSubject.asObservable() ) + input.newProfileImage + .compactMap { $0 } + .subscribe(onNext: { [weak self] image in + self?.viewModel.updateProfileImage(image) + }) + .disposed(by: disposeBag) + let output = viewModel.transform(input: input, disposeBag: disposeBag) output.profileImage diff --git a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift index 7a20602a..6839ef38 100644 --- a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift +++ b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift @@ -15,6 +15,7 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { private let rootView = MyPageView() private let viewModel = MyPageViewModel() private let disposeBag = DisposeBag() + private var needsUserInfoRefresh = true override func loadView() { view = rootView @@ -22,7 +23,10 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - viewModel.fetchUserInfo() + if needsUserInfoRefresh { + viewModel.fetchUserInfo() + needsUserInfoRefresh = false + } } override func viewDidLoad() { @@ -30,7 +34,6 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { view.backgroundColor = .green1 bindViewModel() - viewModel.fetchUserInfo() } override func setupView() { @@ -105,18 +108,18 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { .disposed(by: disposeBag) viewModel.logoutResult - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] result in - switch result { - case .success: - print("Logout successful") - self?.navigateToLoginScreen() - case .failure(let error): - print("Logout failed: \(error)") - } - }) - .disposed(by: disposeBag) - + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] result in + switch result { + case .success: + print("Logout successful") + self?.navigateToLoginScreen() + case .failure(let error): + print("Logout failed: \(error)") + } + }) + .disposed(by: disposeBag) + viewModel.userInfo .observe(on: MainScheduler.instance) @@ -138,31 +141,31 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { } private func updateProfileImage(with urlString: String?) { - print("Attempting to update profile image with URL: \(urlString ?? "nil")") - if let urlString = urlString, let url = URL(string: urlString) { - rootView.contentView.profileImageView.kf.setImage( - with: url, - placeholder: UIImage.imgProfile, - options: [ - .transition(.fade(0.2)), - .forceRefresh, - .cacheOriginalImage - ], - completionHandler: { result in - switch result { - case .success(let value): - print("Profile image loaded successfully. Size: \(value.image.size)") - case .failure(let error): - print("Failed to load profile image: \(error.localizedDescription)") - self.rootView.contentView.profileImageView.image = UIImage.imgProfile - } + print("Attempting to update profile image with URL: \(urlString ?? "nil")") + if let urlString = urlString, let url = URL(string: urlString) { + rootView.contentView.profileImageView.kf.setImage( + with: url, + placeholder: UIImage.imgProfile, + options: [ + .transition(.fade(0.2)), + .forceRefresh, + .cacheOriginalImage + ], + completionHandler: { result in + switch result { + case .success(let value): + print("Profile image loaded successfully. Size: \(value.image.size)") + case .failure(let error): + print("Failed to load profile image: \(error.localizedDescription)") + self.rootView.contentView.profileImageView.image = UIImage.imgProfile } - ) - } else { - print("Invalid URL or nil. Setting default profile image.") - rootView.contentView.profileImageView.image = UIImage.imgProfile - } + } + ) + } else { + print("Invalid URL or nil. Setting default profile image.") + rootView.contentView.profileImageView.image = UIImage.imgProfile } + } private func loadImage(from urlString: String, into imageView: UIImageView) { guard let url = URL(string: urlString) else { @@ -200,34 +203,23 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { } private func pushEditProfileViewController() { - let authService = AuthService() - let editProfileViewModel = MyPageEditViewModel(authService: authService) - let editProfileViewController = MyPageEditViewController(viewModel: editProfileViewModel) - - editProfileViewModel.profileImageUpdated - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] imageDataString in - print("Profile image update received. Data string length: \(imageDataString?.count ?? 0)") - if let imageDataString = imageDataString, - let imageData = Data(base64Encoded: imageDataString), - let image = UIImage(data: imageData) { - print("Successfully created UIImage from updated profile data") - self?.rootView.contentView.profileImageView.image = image - // Force refresh Kingfisher cache - KingfisherManager.shared.cache.removeImage(forKey: self?.viewModel.userInfo.value?.profileImageURL ?? "") - } else { - print("Failed to create UIImage from updated profile data. Setting default image.") - self?.rootView.contentView.profileImageView.image = UIImage.imgProfile - } - // Refresh user info to get the updated profile image URL - self?.viewModel.fetchUserInfo() - }) - .disposed(by: disposeBag) - - editProfileViewController.hidesBottomBarWhenPushed = true - - navigationController?.pushViewController(editProfileViewController, animated: true) - } + let editViewModel = MyPageEditViewModel(authService: AuthService()) + let editVC = MyPageEditViewController(viewModel: editViewModel) + + editViewModel.profileImageUpdated + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] imageDataString in + if let imageDataString = imageDataString, + let imageData = Data(base64Encoded: imageDataString), + let image = UIImage(data: imageData) { + self?.rootView.contentView.profileImageView.image = image + } + self?.needsUserInfoRefresh = true + }) + .disposed(by: disposeBag) + + navigationController?.pushViewController(editVC, animated: true) + } private func pushAskViewController() { let askViewController = MyPageAskViewController(viewModel: self.viewModel) @@ -240,12 +232,12 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { } private func navigateToLoginScreen() { - let loginViewModel = LoginViewModel() - let loginViewController = LoginViewController(viewModel: loginViewModel) - let navigationController = UINavigationController(rootViewController: loginViewController) - navigationController.modalPresentationStyle = .fullScreen - self.view.window?.rootViewController = navigationController - self.view.window?.makeKeyAndVisible() + let loginViewModel = LoginViewModel() + let loginViewController = LoginViewController(viewModel: loginViewModel) + let navigationController = UINavigationController(rootViewController: loginViewController) + navigationController.modalPresentationStyle = .fullScreen + self.view.window?.rootViewController = navigationController + self.view.window?.makeKeyAndVisible() } func actionButtonDidTap(for kind: ActionSheetKind) { diff --git a/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift b/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift index fc66f8fb..35c1137d 100644 --- a/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift +++ b/KkuMulKum/Source/MyPage/ViewModel/MyPageEditViewModel.swift @@ -16,6 +16,7 @@ class MyPageEditViewModel: ViewModelType { private let userInfo = BehaviorRelay(value: nil) let profileImageUpdated = PublishSubject() + struct Input { let profileImageTap: Observable let confirmButtonTap: Observable @@ -130,6 +131,32 @@ class MyPageEditViewModel: ViewModelType { ) } + func updateProfileImage(_ image: UIImage) { + guard let imageData = image.jpegData(compressionQuality: 0.8) else { + print("Failed to convert image to data") + return + } + + Task { + do { + let _: EmptyModel = try await self.authService.performRequest( + .updateProfileImage( + image: imageData, + fileName: "profile_image.jpg", + mimeType: "image/jpeg" + ) + ) + DispatchQueue.main.async { [weak self] in + self?.profileImageUpdated.onNext(imageData.base64EncodedString()) + } + } catch { + let networkError = error as? NetworkError ?? .unknownError("오류 발생.") + print("Profile image upload failed: \(networkError)") + self.profileImageUpdated.onNext(nil) + } + } + } + func fetchUserInfo() { Task { do { From dd5ead43e3d105c4340084e6080a69e52e3f114e Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 6 Sep 2024 00:24:58 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix/#363=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?+=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=82=B9?= =?UTF-8?q?=ED=94=BC=EC=85=94=EA=B4=80=EB=A0=A8=20=EB=AA=A8=EB=93=A0?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/MyPageViewController.swift | 22 +++++--- .../ProfileSetupViewController.swift | 18 +++--- .../ViewModel/ProfileSetupViewModel.swift | 55 +++++++------------ 3 files changed, 41 insertions(+), 54 deletions(-) diff --git a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift index 6839ef38..868bc614 100644 --- a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift +++ b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift @@ -140,12 +140,16 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { updateProfileImage(with: userInfo.profileImageURL) } - private func updateProfileImage(with urlString: String?) { + private func updateProfileImage(with urlString: String?, localImage: UIImage? = nil) { print("Attempting to update profile image with URL: \(urlString ?? "nil")") + if let localImage = localImage { + rootView.contentView.profileImageView.image = localImage + } + if let urlString = urlString, let url = URL(string: urlString) { rootView.contentView.profileImageView.kf.setImage( with: url, - placeholder: UIImage.imgProfile, + placeholder: localImage ?? UIImage.imgProfile, options: [ .transition(.fade(0.2)), .forceRefresh, @@ -154,16 +158,18 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { completionHandler: { result in switch result { case .success(let value): - print("Profile image loaded successfully. Size: \(value.image.size)") + print("Profile image loaded successfully from server. Size: \(value.image.size)") case .failure(let error): - print("Failed to load profile image: \(error.localizedDescription)") - self.rootView.contentView.profileImageView.image = UIImage.imgProfile + print("Failed to load profile image from server: \(error.localizedDescription)") + if self.rootView.contentView.profileImageView.image == nil { + self.rootView.contentView.profileImageView.image = UIImage.imgProfile + } } } ) } else { - print("Invalid URL or nil. Setting default profile image.") - rootView.contentView.profileImageView.image = UIImage.imgProfile + print("Invalid URL or nil. Using local image or default profile image.") + rootView.contentView.profileImageView.image = localImage ?? UIImage.imgProfile } } @@ -212,7 +218,7 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { if let imageDataString = imageDataString, let imageData = Data(base64Encoded: imageDataString), let image = UIImage(data: imageData) { - self?.rootView.contentView.profileImageView.image = image + self?.updateProfileImage(with: nil, localImage: image) } self?.needsUserInfoRefresh = true }) diff --git a/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift b/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift index aa5086a4..c2f46519 100644 --- a/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift +++ b/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift @@ -50,21 +50,17 @@ class ProfileSetupViewController: BaseViewController { } @objc private func confirmButtonTapped() { - Task { - let success = await viewModel.uploadProfileImage() - if success { - DispatchQueue.main.async { - let welcomeVC = WelcomeViewController( - viewModel: WelcomeViewModel(nickname: self.viewModel.nickname) - ) - welcomeVC.modalPresentationStyle = .fullScreen - self.present(welcomeVC, animated: true, completion: nil) - } - } + if viewModel.isConfirmButtonEnabled.value { + viewModel.uploadProfileImage() + navigateToWelcomeScreen() } } @objc private func skipButtonTapped() { + navigateToWelcomeScreen() + } + + private func navigateToWelcomeScreen() { let welcomeVC = WelcomeViewController( viewModel: WelcomeViewModel(nickname: viewModel.nickname) ) diff --git a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift index ff0611b2..44a60828 100644 --- a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift @@ -48,43 +48,28 @@ class ProfileSetupViewModel { return imageData } - func uploadProfileImage() async -> Bool { - print("uploadProfileImage 함수 호출됨") - guard let imageData = imageData else { - print("이미지 데이터가 없습니다.") - serverResponse.value = "이미지 데이터가 없습니다." - return false - } - - if imageData.count > maxImageSizeBytes { - print("이미지 크기가 최대 허용 크기를 초과합니다.") - serverResponse.value = "이미지 크기가 너무 큽니다. 더 작은 이미지를 선택해주세요." - return false - } - - print("업로드할 이미지 데이터 크기: \(imageData.count) bytes") - - let fileName = "profile_image.jpg" - let mimeType = "image/jpeg" - - do { - let _: EmptyModel = try await authService.performRequest( - .updateProfileImage( - image: imageData, - fileName: fileName, - mimeType: mimeType - ) - ) - serverResponse.value = "프로필 이미지가 성공적으로 업로드되었습니다." - print("프로필 이미지 업로드 성공") + func uploadProfileImage() { + guard let imageData = imageData else { + serverResponse.value = "이미지 데이터가 없습니다." + return + } - clearImageCache() - return true - } catch { - handleError(error as? NetworkError ?? .unknownError("알 수 없는 오류가 발생했습니다.")) - return false + Task { + do { + let _: EmptyModel = try await authService.performRequest( + .updateProfileImage( + image: imageData, + fileName: "profile_image.jpg", + mimeType: "image/jpeg" + ) + ) + print("프로필 이미지가 성공적으로 업로드되었습니다.") + clearImageCache() + } catch { + print("프로필 이미지 업로드 실패: \(error.localizedDescription)") + } + } } - } private func handleError(_ error: NetworkError) { switch error { From 7e72d63fe4e8c84116ed0054004064b9819399ed Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 12:26:25 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix/#373=20=ED=94=84=EB=A1=9C=EB=B9=84?= =?UTF-8?q?=EC=A0=80=EB=8B=9D=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 5052995f..5bb000ab 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -2270,7 +2270,7 @@ PRODUCT_BUNDLE_IDENTIFIER = KkuMulKum.yizihn; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = KkumulkumRelease; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = KkumulkumRelease1; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; From 845a559cb19d4fc89f3542d61bb0ea4a67151063 Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 12:27:01 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor/#373=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20=EB=B0=B0=EA=B2=BD=20?= =?UTF-8?q?=ED=9D=B0=EC=83=89=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum/Resource/Base/BaseViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/KkuMulKum/Resource/Base/BaseViewController.swift b/KkuMulKum/Resource/Base/BaseViewController.swift index 570a75db..7b647d68 100644 --- a/KkuMulKum/Resource/Base/BaseViewController.swift +++ b/KkuMulKum/Resource/Base/BaseViewController.swift @@ -44,6 +44,13 @@ extension BaseViewController { .font: UIFont.pretendard(.body03) ] + let barAppearance = UINavigationBarAppearance() + barAppearance.backgroundColor = .white + barAppearance.shadowColor = nil + + navigationController?.navigationBar.standardAppearance = barAppearance + navigationController?.navigationBar.scrollEdgeAppearance = barAppearance + if !isBorderHidden { addBorder() } From 11d4355487fac0a3876d77605f8b6bb028982986 Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 12:34:55 +0900 Subject: [PATCH 07/13] =?UTF-8?q?chore/#373=20'self'=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 옵셔널 바인딩을 한 'self'는 코드 블럭 영역 내에서 암시적으로 사용됨. - 따라서 명시할 필요 X --- .../Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift b/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift index f5ac6d14..2de15d98 100644 --- a/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift +++ b/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift @@ -123,7 +123,7 @@ extension MeetingInfoViewModel: ViewModelType { return Driver.just(false) } - return self.service.exitMeeting(with: self.meetingID) + return service.exitMeeting(with: meetingID) .map { $0.success } .asDriver(onErrorJustReturn: false) } From d057715f85f31dea852420da9cd9537705d99c2d Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 12:51:31 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor/#373=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EB=82=B4=20=EC=95=BD=EC=86=8D=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20RxMoya=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Service/MeetingService.swift | 23 ++++++++++++++----- .../Service/MeetingInfoService.swift | 5 +++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/KkuMulKum/Network/Service/MeetingService.swift b/KkuMulKum/Network/Service/MeetingService.swift index 61000c1e..20ff4804 100644 --- a/KkuMulKum/Network/Service/MeetingService.swift +++ b/KkuMulKum/Network/Service/MeetingService.swift @@ -57,11 +57,22 @@ extension MeetingService: MeetingInfoServiceProtocol { func fetchMeetingPromiseList( with meetingID: Int, isParticipant: Bool? - ) async throws -> ResponseBodyDTO? { - guard let isParticipant else { - return try await request(with: .fetchMeetingPromiseList(meetingID: meetingID)) + ) -> Single> { + if let isParticipant { + return provider.rx.request(.fetchParticipatedPromiseList(meetingID: meetingID, isParticipant: isParticipant)) + .map(ResponseBodyDTO.self) + .catch { error in + print(">>> 에러 발생: \(error.localizedDescription) : \(#function) : \(Self.self)") + return .error(error) + } } - return try await request(with: .fetchParticipatedPromiseList(meetingID: meetingID, isParticipant: isParticipant)) + + return provider.rx.request(.fetchMeetingPromiseList(meetingID: meetingID)) + .map(ResponseBodyDTO.self) + .catch { error in + print(">>> 에러 발생: \(error.localizedDescription) : \(#function) : \(Self.self)") + return .error(error) + } } func exitMeeting(with meetingID: Int) -> Single> { @@ -187,7 +198,7 @@ final class MockMeetingInfoService: MeetingInfoServiceProtocol { func fetchMeetingPromiseList( with meetingID: Int, isParticipant: Bool? - ) async throws -> ResponseBodyDTO? { + ) -> Single> { let mockData = MeetingPromisesModel( promises: [ MeetingPromise(promiseID: 1,name: "꾸물 리프레시 데이",dDay: 0,time: "PM 2:00",placeName: "DMC역"), @@ -208,7 +219,7 @@ final class MockMeetingInfoService: MeetingInfoServiceProtocol { ] ) - return ResponseBodyDTO(success: true, data: mockData, error: nil) + return .just(ResponseBodyDTO(success: true, data: mockData, error: nil)) } func exitMeeting(with meetingID: Int) -> Single> { diff --git a/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift b/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift index d3ed092b..2489ea98 100644 --- a/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift +++ b/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift @@ -12,6 +12,9 @@ import RxSwift protocol MeetingInfoServiceProtocol { func fetchMeetingInfo(with meetingID: Int) async throws -> ResponseBodyDTO? func fetchMeetingMemberList(with meetingID: Int) async throws -> ResponseBodyDTO? - func fetchMeetingPromiseList(with meetingID: Int, isParticipant: Bool?) async throws -> ResponseBodyDTO? + func fetchMeetingPromiseList( + with meetingID: Int, + isParticipant: Bool? + ) -> Single> func exitMeeting(with meetingID: Int) -> Single> } From 433549554d904e293c49b9ae0618bbf08d652aa6 Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 12:52:30 +0900 Subject: [PATCH 09/13] =?UTF-8?q?chore/#373=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD,?= =?UTF-8?q?=20=EC=84=B8=EA=B7=B8=EB=A8=BC=ED=8A=B8=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum/Source/MeetingInfo/View/MeetingInfoView.swift | 4 +++- .../MeetingInfo/ViewModel/MeetingInfoViewModel.swift | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/KkuMulKum/Source/MeetingInfo/View/MeetingInfoView.swift b/KkuMulKum/Source/MeetingInfo/View/MeetingInfoView.swift index e802e016..480ae715 100644 --- a/KkuMulKum/Source/MeetingInfo/View/MeetingInfoView.swift +++ b/KkuMulKum/Source/MeetingInfo/View/MeetingInfoView.swift @@ -74,7 +74,9 @@ final class MeetingInfoView: BaseView { $0.setText("남은 약속을 확인해보세요", style: .body01, color: .gray7) } - private let segmentedControl = UnderlineSegmentedControl(items: ["내가 속한 약속", "모든 약속"]) + private let segmentedControl = UnderlineSegmentedControl(items: ["내가 속한 약속", "모든 약속"]).then { + $0.selectedSegmentIndex = 0 + } private let emptyDescriptionView = UIView(backgroundColor: .white).then { $0.layer.cornerRadius = 8 diff --git a/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift b/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift index 2de15d98..75a5b942 100644 --- a/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift +++ b/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift @@ -15,13 +15,12 @@ final class MeetingInfoViewModel { var meetingName: String { infoRelay.value?.name ?? "" } var meetingInvitationCode: String? { infoRelay.value?.invitationCode } - var meetingPromises: [MeetingPromise] { meetingPromisesModelRelay.value?.promises ?? [] } private let service: MeetingInfoServiceProtocol private let infoRelay = BehaviorRelay(value: nil) private let meetingMemberModelRelay = BehaviorRelay(value: nil) private let meetingPromisesModelRelay = BehaviorRelay(value: nil) - private let partipatedPromisesModelRelay = BehaviorRelay(value: nil) + private let participatedPromisesModelRelay = BehaviorRelay(value: nil) init(meetingID: Int, service: MeetingInfoServiceProtocol) { self.meetingID = meetingID @@ -109,7 +108,7 @@ extension MeetingInfoViewModel: ViewModelType { return Observable.just([]) } - let source = index == 0 ? self.partipatedPromisesModelRelay : self.meetingPromisesModelRelay + let source = index == 0 ? self.participatedPromisesModelRelay : self.meetingPromisesModelRelay return source .compactMap { $0?.promises } .map { self.convertToMeetingInfoPromiseModels(from: $0) } @@ -135,7 +134,7 @@ extension MeetingInfoViewModel: ViewModelType { } .map { [weak self] selectedIndex, selectedItem in let promises = selectedIndex == 0 - ? self?.partipatedPromisesModelRelay.value?.promises + ? self?.participatedPromisesModelRelay.value?.promises : self?.meetingPromisesModelRelay.value?.promises return promises?[selectedItem].promiseID From 9030dbae84492a6627b796cf557619f722e8b71f Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 14:08:29 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore/#373=20=EB=B9=88=20=EC=A4=84=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift b/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift index e07306af..1f25daff 100644 --- a/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift +++ b/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift @@ -19,4 +19,3 @@ struct ErrorResponse: Codable { let code: Int let message: String } - From 302bb0dd628aa638563d13ab39d7836b757b1256 Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 14:08:59 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor/#373=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=95=BD=EC=86=8D=EA=B3=BC=20=EB=82=B4?= =?UTF-8?q?=EA=B0=80=20=EC=B0=B8=EC=97=AC=ED=95=9C=20=EC=95=BD=EC=86=8D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Service/MeetingService.swift | 160 ++---------------- .../Service/MeetingInfoService.swift | 5 +- 2 files changed, 15 insertions(+), 150 deletions(-) diff --git a/KkuMulKum/Network/Service/MeetingService.swift b/KkuMulKum/Network/Service/MeetingService.swift index 20ff4804..1d71db71 100644 --- a/KkuMulKum/Network/Service/MeetingService.swift +++ b/KkuMulKum/Network/Service/MeetingService.swift @@ -54,20 +54,20 @@ extension MeetingService: MeetingInfoServiceProtocol { return try await request(with: .fetchMeetingMember(meetingID: meetingID)) } - func fetchMeetingPromiseList( + func fetchMeetingPromiseList(with meetingID: Int) -> Single> { + return provider.rx.request(.fetchMeetingPromiseList(meetingID: meetingID)) + .map(ResponseBodyDTO.self) + .catch { error in + print(">>> 에러 발생: \(error.localizedDescription) : \(#function) : \(Self.self)") + return .error(error) + } + } + + func fetchParticipatedPromiseList( with meetingID: Int, - isParticipant: Bool? + isParticipant: Bool ) -> Single> { - if let isParticipant { - return provider.rx.request(.fetchParticipatedPromiseList(meetingID: meetingID, isParticipant: isParticipant)) - .map(ResponseBodyDTO.self) - .catch { error in - print(">>> 에러 발생: \(error.localizedDescription) : \(#function) : \(Self.self)") - return .error(error) - } - } - - return provider.rx.request(.fetchMeetingPromiseList(meetingID: meetingID)) + return provider.rx.request(.fetchParticipatedPromiseList(meetingID: meetingID, isParticipant: isParticipant)) .map(ResponseBodyDTO.self) .catch { error in print(">>> 에러 발생: \(error.localizedDescription) : \(#function) : \(Self.self)") @@ -101,139 +101,3 @@ extension MeetingService: InviteCodeServiceProtocol { return try await self.request(with: .joinMeeting(request: request)) } } - -final class MockMeetingInfoService: MeetingInfoServiceProtocol { - func fetchMeetingInfo(with meetingID: Int) -> ResponseBodyDTO? { - let mockData = MeetingInfoModel( - meetingID: 1, - name: "웅웅난진웅", - createdAt: "2024.06.08", - metCount: 3, - invitationCode: "WD56CQ" - ) - - return ResponseBodyDTO(success: true, data: mockData, error: nil) - } - - func fetchMeetingMemberList(with meetingID: Int) -> ResponseBodyDTO? { - let mockData = MeetingMembersModel( - memberCount: 14, - members: [ - Member( - memberID: 1, - name: "김진웅", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 2, - name: "김수연", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 3, - name: "이지훈", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 4, - name: "이유진", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 5, - name: "이승현", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 6, - name: "허준혁", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 7, - name: "배차은우", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 8, - name: "김윤서", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 9, - name: "정혜진", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 10, - name: "주효은", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 11, - name: "박상준", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 12, - name: "김채원", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 13, - name: "류희재", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ), - Member( - memberID: 14, - name: "김민지", - profileImageURL: "https://reqres.in/img/faces/\(Int.random(in: 1...10))-image.jpg" - ) - ] - ) - - return ResponseBodyDTO(success: true, data: mockData, error: nil) - } - - func fetchMeetingPromiseList( - with meetingID: Int, - isParticipant: Bool? - ) -> Single> { - let mockData = MeetingPromisesModel( - promises: [ - MeetingPromise(promiseID: 1,name: "꾸물 리프레시 데이",dDay: 0,time: "PM 2:00",placeName: "DMC역"), - MeetingPromise(promiseID: 2,name: "꾸물 잼얘 나이트",dDay: 10,time: "PM 6:00",placeName: "홍대입구"), - MeetingPromise(promiseID: 3,name: "친구 생일 파티",dDay: 5,time: "PM 7:00",placeName: "강남역"), - MeetingPromise(promiseID: 4,name: "주말 산책",dDay: 3,time: "AM 10:00",placeName: "서울숲"), - MeetingPromise(promiseID: 5,name: "프로젝트 미팅",dDay: 1,time: "AM 9:00",placeName: "삼성역"), - MeetingPromise(promiseID: 6,name: "독서 모임",dDay: 7,time: "PM 3:00",placeName: "합정역"), - MeetingPromise(promiseID: 7,name: "헬스클럽 모임",dDay: 2,time: "AM 8:00",placeName: "신촌역"), - MeetingPromise(promiseID: 8,name: "영화 관람",dDay: 4,time: "PM 8:00",placeName: "잠실역"), - MeetingPromise(promiseID: 9,name: "저녁 식사",dDay: 6,time: "PM 7:30",placeName: "이태원역"), - MeetingPromise(promiseID: 10,name: "아침 조깅",dDay: 14,time: "AM 6:00",placeName: "한강공원"), - MeetingPromise(promiseID: 11,name: "커피 브레이크",dDay: 8,time: "PM 4:00",placeName: "을지로입구"), - MeetingPromise(promiseID: 12,name: "스터디 그룹",dDay: 12,time: "PM 5:00",placeName: "강남역"), - MeetingPromise(promiseID: 13,name: "뮤직 페스티벌",dDay: 9,time: "PM 2:00",placeName: "난지공원"), - MeetingPromise(promiseID: 14, name: "낚시 여행", dDay: 11, time: "AM 5:00", placeName: "속초항"), - MeetingPromise(promiseID: 15, name: "가족 모임", dDay: 13, time: "PM 1:00", placeName: "광화문역") - ] - ) - - return .just(ResponseBodyDTO(success: true, data: mockData, error: nil)) - } - - func exitMeeting(with meetingID: Int) -> Single> { - let falseResponse = ResponseBodyDTO(success: false, data: nil, error: nil) - return .just(falseResponse) - } -} - -final class MockInviteCodeService: InviteCodeServiceProtocol { - func joinMeeting(with request: RegisterMeetingsModel) -> ResponseBodyDTO? { - let mockData = RegisterMeetingsResponseModel( - meetingID: 1 - ) - - return ResponseBodyDTO.init(success: true, data: mockData, error: nil) - } -} diff --git a/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift b/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift index 2489ea98..f7ba6910 100644 --- a/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift +++ b/KkuMulKum/Source/MeetingInfo/Service/MeetingInfoService.swift @@ -12,9 +12,10 @@ import RxSwift protocol MeetingInfoServiceProtocol { func fetchMeetingInfo(with meetingID: Int) async throws -> ResponseBodyDTO? func fetchMeetingMemberList(with meetingID: Int) async throws -> ResponseBodyDTO? - func fetchMeetingPromiseList( + func fetchMeetingPromiseList(with meetingID: Int) -> Single> + func fetchParticipatedPromiseList( with meetingID: Int, - isParticipant: Bool? + isParticipant: Bool ) -> Single> func exitMeeting(with meetingID: Int) -> Single> } From 741a2752e2c2ca06a45ef5daad29ec09e99f3177 Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 15:56:01 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor/#373=20=EC=84=B8=EA=B7=B8?= =?UTF-8?q?=EB=A8=BC=ED=8A=B8=EA=B0=80=20=EB=B3=80=EA=B2=BD=EB=90=A0=20?= =?UTF-8?q?=EB=95=8C=EB=A7=88=EB=8B=A4=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=83=88=EB=A1=9C=20?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MeetingInfo/Cell/MeetingMemberCell.swift | 56 +++++++------------ .../MeetingInfoViewController.swift | 2 +- 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/KkuMulKum/Source/MeetingInfo/Cell/MeetingMemberCell.swift b/KkuMulKum/Source/MeetingInfo/Cell/MeetingMemberCell.swift index 3974ae46..29e9e819 100644 --- a/KkuMulKum/Source/MeetingInfo/Cell/MeetingMemberCell.swift +++ b/KkuMulKum/Source/MeetingInfo/Cell/MeetingMemberCell.swift @@ -12,13 +12,13 @@ import SnapKit import Then protocol MeetingMemberCellDelegate: AnyObject { - func profileImageButtonDidTap() + func profileImageViewDidTap() } final class MeetingMemberCell: BaseCollectionViewCell { - private let profileImageButton = UIButton().then { + private let profileImageView: UIImageView = UIImageView().then { + $0.isUserInteractionEnabled = true $0.layer.cornerRadius = Screen.height(64) / 2 - $0.isEnabled = false $0.clipsToBounds = true } @@ -28,47 +28,34 @@ final class MeetingMemberCell: BaseCollectionViewCell { } private weak var delegate: MeetingMemberCellDelegate? + private lazy var tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageViewDidTap(_:))) override func prepareForReuse() { super.prepareForReuse() - profileImageButton.do { - var config = UIButton.Configuration.plain() - config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) - - $0.imageView?.image = nil - $0.backgroundColor = .clear - $0.isEnabled = false - } + profileImageView.image = nil + profileImageView.removeGestureRecognizer(tapGesture) nameLabel.setText(style: .caption02, color: .gray6) } override func setupView() { - contentView.addSubviews(profileImageButton, nameLabel) + contentView.addSubviews(profileImageView, nameLabel) } override func setupAutoLayout() { - profileImageButton.snp.makeConstraints { + profileImageView.snp.makeConstraints { $0.top.centerX.equalToSuperview() $0.height.equalTo(Screen.height(64)) - $0.width.equalTo(profileImageButton.snp.height) + $0.width.equalTo(profileImageView.snp.height) } nameLabel.snp.makeConstraints { - $0.top.equalTo(profileImageButton.snp.bottom).offset(4) - $0.centerX.equalTo(profileImageButton) + $0.top.equalTo(profileImageView.snp.bottom).offset(4) + $0.centerX.equalTo(profileImageView) $0.bottom.equalToSuperview() } } - - override func setupAction() { - profileImageButton.addTarget( - self, - action: #selector(profileImageButtonDidTap(_:)), - for: .touchUpInside - ) - } } extension MeetingMemberCell { @@ -91,15 +78,12 @@ private extension MeetingMemberCell { func configureForAdd(with delegate: MeetingMemberCellDelegate) { self.delegate = delegate - profileImageButton.do { - var config = UIButton.Configuration.plain() - config.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0) - - $0.configuration = config + profileImageView.do { + $0.image = .iconPlusDark.withRenderingMode(.alwaysOriginal) + $0.contentMode = .center $0.backgroundColor = .gray1 - $0.setImage(.iconPlus.withTintColor(.gray4), for: .normal) - $0.isEnabled = true } + profileImageView.addGestureRecognizer(tapGesture) nameLabel.setText(style: .caption02, color: .gray6) } @@ -110,15 +94,15 @@ private extension MeetingMemberCell { nameLabel.setText(name ?? " ", style: .caption02, color: .gray6) - profileImageButton.kf.setImage( + profileImageView.kf.setImage( with: imageURL, - for: .disabled, - placeholder: .imgProfile.withRenderingMode(.alwaysOriginal) + placeholder: UIImage(resource: .imgProfile).withRenderingMode(.alwaysOriginal) ) + profileImageView.contentMode = .scaleAspectFill } @objc - func profileImageButtonDidTap(_ sender: UIButton) { - delegate?.profileImageButtonDidTap() + func profileImageViewDidTap(_ sender: UIImageView) { + delegate?.profileImageViewDidTap() } } diff --git a/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift b/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift index 66836109..64044338 100644 --- a/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift +++ b/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift @@ -154,7 +154,7 @@ private extension MeetingInfoViewController { output.navigateToPromiseInfo .drive(with: self) { owner, promiseID in - guard let promiseID else { return } + guard promiseID > 0 else { return } let pagePromiseViewController = PromiseViewController( viewModel: PromiseViewModel(promiseID: promiseID, service: PromiseService()) From c7848c448d754d9ddd89ade3fe4121f4b44aa230 Mon Sep 17 00:00:00 2001 From: JinUng41 Date: Mon, 9 Sep 2024 15:56:30 +0900 Subject: [PATCH 13/13] =?UTF-8?q?fix/#373=20=EC=95=BD=EC=86=8D=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=EB=B6=80?= =?UTF-8?q?=ED=84=B0=20=EB=90=98=EB=8F=8C=EC=95=84=EC=99=94=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C,=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EA=B0=80=20=ED=99=95?= =?UTF-8?q?=EB=8C=80=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MeetingInfoViewController.swift | 2 +- .../ViewModel/MeetingInfoViewModel.swift | 99 ++++++++----------- 2 files changed, 44 insertions(+), 57 deletions(-) diff --git a/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift b/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift index 64044338..8d4ffd97 100644 --- a/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift +++ b/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift @@ -223,7 +223,7 @@ extension MeetingInfoViewController: CustomActionSheetDelegate { // MARK: - MeetingMemberCellDelegate extension MeetingInfoViewController: MeetingMemberCellDelegate { - func profileImageButtonDidTap() { + func profileImageViewDidTap() { guard let code = viewModel.meetingInvitationCode else { return } let viewController = InvitationCodePopUpViewController( diff --git a/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift b/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift index 75a5b942..4adaab83 100644 --- a/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift +++ b/KkuMulKum/Source/MeetingInfo/ViewModel/MeetingInfoViewModel.swift @@ -9,6 +9,7 @@ import Foundation import RxCocoa import RxSwift +import Then final class MeetingInfoViewModel { let meetingID: Int @@ -16,11 +17,16 @@ final class MeetingInfoViewModel { var meetingName: String { infoRelay.value?.name ?? "" } var meetingInvitationCode: String? { infoRelay.value?.invitationCode } + private let dateFormatter = DateFormatter().then { + $0.locale = Locale(identifier: "ko_KR") + $0.timeZone = TimeZone(identifier: "Asia/Seoul") + $0.dateFormat = "yyyy-MM-dd HH:mm:ss" + $0.amSymbol = "AM" + $0.pmSymbol = "PM" + } private let service: MeetingInfoServiceProtocol private let infoRelay = BehaviorRelay(value: nil) private let meetingMemberModelRelay = BehaviorRelay(value: nil) - private let meetingPromisesModelRelay = BehaviorRelay(value: nil) - private let participatedPromisesModelRelay = BehaviorRelay(value: nil) init(meetingID: Int, service: MeetingInfoServiceProtocol) { self.meetingID = meetingID @@ -43,7 +49,7 @@ extension MeetingInfoViewModel: ViewModelType { let members: Driver<[Member]> let promises: Driver<[MeetingInfoPromiseModel]> let isExitMeetingSucceed: Driver - let navigateToPromiseInfo: Driver + let navigateToPromiseInfo: Driver } func transform(input: Input, disposeBag: DisposeBag) -> Output { @@ -51,19 +57,18 @@ extension MeetingInfoViewModel: ViewModelType { .subscribe(with: self) { owner, _ in owner.fetchMeetingInfo() owner.fetchMeetingMembers() - owner.fetchMeetingPromises() - owner.fetchParticipatedPromises() } .disposed(by: disposeBag) let info = infoRelay - .map { info -> MeetingInfoModel? in - guard let info else { - print("MeetingInfoModel이 존재하지 않습니다.") + .map { [weak self] info -> MeetingInfoModel? in + guard let self, + let info + else { + print(">>> 모델이 존재하지 않습니다. : \(#function) : \(Self.self)") return nil } - let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" guard let date = dateFormatter.date(from: info.createdAt) else { @@ -102,20 +107,40 @@ extension MeetingInfoViewModel: ViewModelType { } .asDriver(onErrorJustReturn: []) - let promises = input.selectedSegmentedIndex + let promises = Observable.merge( + input.viewWillAppear.withLatestFrom(input.selectedSegmentedIndex), + input.selectedSegmentedIndex + ) + .startWith(0) .flatMapLatest { [weak self] index -> Observable<[MeetingInfoPromiseModel]> in guard let self else { - return Observable.just([]) + return .just([]) } - let source = index == 0 ? self.participatedPromisesModelRelay : self.meetingPromisesModelRelay - return source - .compactMap { $0?.promises } - .map { self.convertToMeetingInfoPromiseModels(from: $0) } - .asObservable() + if index == 0 { + return service.fetchParticipatedPromiseList(with: self.meetingID, isParticipant: true) + .map { response in + self.convertToMeetingInfoPromiseModels(from: response.data?.promises ?? []) + } + .asObservable() + .catchAndReturn([]) + } else { + return service.fetchMeetingPromiseList(with: self.meetingID) + .map { response in + self.convertToMeetingInfoPromiseModels(from: response.data?.promises ?? []) + } + .asObservable() + .catchAndReturn([]) + } } .asDriver(onErrorJustReturn: []) + let navigateToPromiseInfo = input.promiseCellDidSelect + .withLatestFrom(promises) { index, promises in + return promises[index].promiseID + } + .asDriver(onErrorJustReturn: -1) + let isExitMeetingSucceed = input.actionButtonDidTapRelay .flatMapLatest { [weak self] _ -> Driver in guard let self else { @@ -128,19 +153,6 @@ extension MeetingInfoViewModel: ViewModelType { } .asDriver(onErrorJustReturn: false) - let navigateToPromiseInfo = input.promiseCellDidSelect - .withLatestFrom(input.selectedSegmentedIndex) { - (selectedIndex: $1, selectedItem: $0) - } - .map { [weak self] selectedIndex, selectedItem in - let promises = selectedIndex == 0 - ? self?.participatedPromisesModelRelay.value?.promises - : self?.meetingPromisesModelRelay.value?.promises - - return promises?[selectedItem].promiseID - } - .asDriver(onErrorJustReturn: nil) - let output = Output( info: info, memberCount: memberCount, @@ -177,33 +189,8 @@ private extension MeetingInfoViewModel { } } - func fetchMeetingPromises() { - Task { - do { - let responseBody = try await service.fetchMeetingPromiseList(with: meetingID, isParticipant: nil) - meetingPromisesModelRelay.accept(responseBody?.data) - } catch { - print(">>> \(error.localizedDescription) : \(#function)") - } - } - } - - func fetchParticipatedPromises() { - Task { - do { - let responseBody = try await service.fetchMeetingPromiseList(with: meetingID, isParticipant: true) - partipatedPromisesModelRelay.accept(responseBody?.data) - } catch { - print(">>> \(error.localizedDescription) : \(#function)") - } - } - } -} - -private extension MeetingInfoViewModel { func convertToMeetingInfoPromiseModels(from promises: [MeetingPromise]) -> [MeetingInfoPromiseModel] { - let inputDateFormatter = DateFormatter() - inputDateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" let outputDateFormatter = DateFormatter().then { $0.locale = Locale(identifier: "ko_KR") @@ -214,7 +201,7 @@ private extension MeetingInfoViewModel { } return promises.compactMap { promise in - guard let date = inputDateFormatter.date(from: promise.time) else { return nil } + guard let date = dateFormatter.date(from: promise.time) else { return nil } let formattedDate = outputDateFormatter.string(from: date) let (dateString, timeString) = splitDateAndTime(from: formattedDate) let (dDayString, state) = configure(dDay: promise.dDay)