diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 5bb000ab..d019b288 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 = UIInterfaceOrientationPortrait; @@ -2255,6 +2256,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "꾸물꿈"; INFOPLIST_KEY_NSCameraUsageDescription = "카메라 사용 권한이 필요합니다."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "사진 라이브러리 접근 권한이 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "꾸물꿈은 카메라 권한을 필요로 합니다. 카메라를 통해 자신의 프로필을 즉시 찍어 업로드할 수 있습니다. 허용 안함 시 일부 기능이 동작하지 않을 수 있습니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 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/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/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 } } diff --git a/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift b/KkuMulKum/Source/MyPage/ViewController/MyPageViewController.swift index 9bf42298..868bc614 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) @@ -137,11 +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, @@ -149,16 +157,19 @@ class MyPageViewController: BaseViewController, CustomActionSheetDelegate { ], completionHandler: { result in switch result { - case .success(_): - print("Profile image loaded successfully") + case .success(let value): + 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 { - 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 } } @@ -198,29 +209,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 - 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 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?.updateProfileImage(with: nil, localImage: image) + } + self?.needsUserInfoRefresh = true + }) + .disposed(by: disposeBag) + + navigationController?.pushViewController(editVC, animated: true) + } private func pushAskViewController() { let askViewController = MyPageAskViewController(viewModel: self.viewModel) @@ -233,12 +238,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 85245200..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 @@ -40,12 +41,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 +63,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 +77,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 +100,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() @@ -122,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 { @@ -143,8 +178,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 "사용자를 찾을 수 없습니다. 로그인 상태를 확인해주세요." } } } 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 {