diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index d07169e8..ab2e0589 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 785AE1D12C3B07A600677CA0 /* PrivacyInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 785AE1D02C3B07A600677CA0 /* PrivacyInfo.plist */; }; 789196362C492F8600FF8CDF /* AuthTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789196352C492F8600FF8CDF /* AuthTargetType.swift */; }; 789196382C49697B00FF8CDF /* AuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789196372C49697B00FF8CDF /* AuthError.swift */; }; + 789196342C486F6B00FF8CDF /* KeychainAccessible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789196332C486F6B00FF8CDF /* KeychainAccessible.swift */; }; 789873322C3D1A7B00435E96 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7898732F2C3D1A7B00435E96 /* LoginViewController.swift */; }; 789873332C3D1A7B00435E96 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789873302C3D1A7B00435E96 /* LoginViewModel.swift */; }; 789873342C3D1A7B00435E96 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789873312C3D1A7B00435E96 /* LoginView.swift */; }; @@ -66,7 +67,6 @@ 78B928782C29402E006D9942 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78B928762C29402E006D9942 /* LaunchScreen.storyboard */; }; 78BD61202C43F557005752FD /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 78BD611F2C43F557005752FD /* SwiftKeychainWrapper */; }; 78BD612B2C4550A6005752FD /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD612A2C4550A6005752FD /* Bundle.swift */; }; - 78BD612F2C4561B9005752FD /* KeychainAccessible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD612E2C4561B9005752FD /* KeychainAccessible.swift */; }; 78BD61342C45B4A7005752FD /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD61332C45B4A7005752FD /* AuthService.swift */; }; 78BD61382C463C8C005752FD /* NicknameModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD61372C463C8C005752FD /* NicknameModel.swift */; }; A39F2B192C47BF83008DA5F5 /* SetReadyCompletedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39F2B182C47BF83008DA5F5 /* SetReadyCompletedView.swift */; }; @@ -241,6 +241,7 @@ 785AE1D02C3B07A600677CA0 /* PrivacyInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.plist; sourceTree = ""; }; 789196352C492F8600FF8CDF /* AuthTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTargetType.swift; sourceTree = ""; }; 789196372C49697B00FF8CDF /* AuthError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthError.swift; sourceTree = ""; }; + 789196332C486F6B00FF8CDF /* KeychainAccessible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAccessible.swift; sourceTree = ""; }; 7898732F2C3D1A7B00435E96 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 789873302C3D1A7B00435E96 /* LoginViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 789873312C3D1A7B00435E96 /* LoginView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; @@ -260,7 +261,6 @@ 78B928772C29402E006D9942 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 78B928792C29402E006D9942 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78BD612A2C4550A6005752FD /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; - 78BD612E2C4561B9005752FD /* KeychainAccessible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAccessible.swift; sourceTree = ""; }; 78BD61332C45B4A7005752FD /* AuthService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; 78BD61372C463C8C005752FD /* NicknameModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameModel.swift; sourceTree = ""; }; A39F2B182C47BF83008DA5F5 /* SetReadyCompletedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetReadyCompletedView.swift; sourceTree = ""; }; @@ -568,19 +568,19 @@ 78AED1322C3D9514000AD80A /* Nickname */ = { isa = PBXGroup; children = ( - 782B40802C3E48BD008B0CA7 /* ViewModel */, 78AED1382C3D98DB000AD80A /* ViewController */, - 78AED1352C3D98C1000AD80A /* NicknameView */, + 782B40802C3E48BD008B0CA7 /* ViewModel */, + 78AED1352C3D98C1000AD80A /* View */, ); path = Nickname; sourceTree = ""; }; - 78AED1352C3D98C1000AD80A /* NicknameView */ = { + 78AED1352C3D98C1000AD80A /* View */ = { isa = PBXGroup; children = ( 78AED1362C3D98D1000AD80A /* NicknameView.swift */, ); - path = NicknameView; + path = View; sourceTree = ""; }; 78AED1382C3D98DB000AD80A /* ViewController */ = { @@ -630,8 +630,8 @@ 78BD612D2C456162005752FD /* KeyChain */ = { isa = PBXGroup; children = ( - 78BD612E2C4561B9005752FD /* KeychainAccessible.swift */, 789D73A62C46AF4900C7077D /* KeychainService.swift */, + 789196332C486F6B00FF8CDF /* KeychainAccessible.swift */, ); path = KeyChain; sourceTree = ""; @@ -1805,7 +1805,6 @@ DE6D4D0F2C3F14D80005584B /* MeetingInfoService.swift in Sources */, DDE7D2C32C470A58005A921F /* ProfileTargetType.swift in Sources */, DD3F9DCC2C485614008E1FF7 /* HomeServiceType.swift in Sources */, - 78BD612F2C4561B9005752FD /* KeychainAccessible.swift in Sources */, DD39768A2C41C2AD00E2A4C4 /* HomeViewController.swift in Sources */, DED5DBF42C34539A006ECE7E /* BaseTableViewCell.swift in Sources */, DDE7D2CA2C47EE81005A921F /* PromiseTargetType.swift in Sources */, @@ -1855,6 +1854,7 @@ DE0137D32C43C5E50088C777 /* MyPageView.swift in Sources */, DE558C592C45954B008DAC4A /* SelectMemberViewController.swift in Sources */, DD4909962C440CDC003ED304 /* ArriveView.swift in Sources */, + 789196342C486F6B00FF8CDF /* KeychainAccessible.swift in Sources */, DD86266A2C4606A300E4F980 /* ReadyStatusService.swift in Sources */, DE254AB02C31195B00A4015E /* NSAttributedString+.swift in Sources */, DD43937B2C412F4500EC1799 /* CreateMeetingViewController.swift in Sources */, diff --git a/KkuMulKum/Network/DTO/Model/Utils/PlaceModel.swift b/KkuMulKum/Network/DTO/Model/Utils/PlaceModel.swift index 1f3d7bd8..18508956 100644 --- a/KkuMulKum/Network/DTO/Model/Utils/PlaceModel.swift +++ b/KkuMulKum/Network/DTO/Model/Utils/PlaceModel.swift @@ -10,6 +10,10 @@ import Foundation /// 네이버 지역 검색 API (Response) struct PlaceModel: ResponseModelType { let places: [Place] + + enum CodingKeys: String, CodingKey { + case places = "locations" + } } struct Place: Codable { diff --git a/KkuMulKum/Network/Service/HomeService.swift b/KkuMulKum/Network/Service/HomeService.swift index c596e0fc..303489f7 100644 --- a/KkuMulKum/Network/Service/HomeService.swift +++ b/KkuMulKum/Network/Service/HomeService.swift @@ -16,7 +16,7 @@ final class HomeService { self.provider = provider } - func request( + func request( with request: HomeTargetType ) async throws -> ResponseBodyDTO? { return try await withCheckedThrowingContinuation { continuation in diff --git a/KkuMulKum/Network/Service/MeetingService.swift b/KkuMulKum/Network/Service/MeetingService.swift index 4adc82fc..bbeb9ad7 100644 --- a/KkuMulKum/Network/Service/MeetingService.swift +++ b/KkuMulKum/Network/Service/MeetingService.swift @@ -16,7 +16,7 @@ final class MeetingService { self.provider = provider } - func request( + func request( with request: MeetingTargetType ) async throws -> ResponseBodyDTO? { return try await withCheckedThrowingContinuation { continuation in diff --git a/KkuMulKum/Network/Service/PromiseService.swift b/KkuMulKum/Network/Service/PromiseService.swift index fe7d4410..f60eff61 100644 --- a/KkuMulKum/Network/Service/PromiseService.swift +++ b/KkuMulKum/Network/Service/PromiseService.swift @@ -16,7 +16,7 @@ final class PromiseService { self.provider = provider } - func request( + func request( with request: PromiseTargetType ) async throws -> ResponseBodyDTO? { return try await withCheckedThrowingContinuation { continuation in @@ -38,5 +38,4 @@ final class PromiseService { } } } - } diff --git a/KkuMulKum/Network/Service/UtilService.swift b/KkuMulKum/Network/Service/UtilService.swift index 4d735a96..6630ba52 100644 --- a/KkuMulKum/Network/Service/UtilService.swift +++ b/KkuMulKum/Network/Service/UtilService.swift @@ -16,7 +16,7 @@ final class UtilService { self.provider = provider } - func request( + func request( with request: UtilTargetType ) async throws -> ResponseBodyDTO? { return try await withCheckedThrowingContinuation { continuation in diff --git a/KkuMulKum/Network/TargetType/UtilTargetType.swift b/KkuMulKum/Network/TargetType/UtilTargetType.swift index eff7fd6c..332a5b2d 100644 --- a/KkuMulKum/Network/TargetType/UtilTargetType.swift +++ b/KkuMulKum/Network/TargetType/UtilTargetType.swift @@ -10,7 +10,7 @@ import Foundation import Moya enum UtilTargetType { - case searchPlaceList(keyword: String) + case searchPlaceList(query: String) } extension UtilTargetType: TargetType { @@ -25,8 +25,8 @@ extension UtilTargetType: TargetType { var path: String { switch self { - case .searchPlaceList(let keyword): - return "/api/v1/locations?q=\(keyword)" + case .searchPlaceList: + return "/api/v1/locations" } } @@ -35,7 +35,10 @@ extension UtilTargetType: TargetType { } var task: Moya.Task { - .requestPlain + switch self { + case .searchPlaceList(let query): + return .requestParameters(parameters: ["q": query], encoding: URLEncoding.queryString) + } } var headers: [String : String]? { diff --git a/KkuMulKum/Resource/KeyChain/KeychainAccessible.swift b/KkuMulKum/Resource/KeyChain/KeychainAccessible.swift index 16320e87..e4757e0f 100644 --- a/KkuMulKum/Resource/KeyChain/KeychainAccessible.swift +++ b/KkuMulKum/Resource/KeyChain/KeychainAccessible.swift @@ -33,8 +33,6 @@ class DefaultKeychainAccessible: KeychainAccessible { return keychain.set(value, forKey: key, withAccessibility: .afterFirstUnlockThisDeviceOnly) } - - func getExpiresIn(_ Key: String) -> Int? { return keychain.integer(forKey: Key) } diff --git a/KkuMulKum/Source/AddPromise/ServiceType/FindPlaceService.swift b/KkuMulKum/Source/AddPromise/ServiceType/FindPlaceService.swift index b30b5009..2f38bcc9 100644 --- a/KkuMulKum/Source/AddPromise/ServiceType/FindPlaceService.swift +++ b/KkuMulKum/Source/AddPromise/ServiceType/FindPlaceService.swift @@ -8,11 +8,17 @@ import Foundation protocol FindPlaceServiceType { - func fetchPlaceList(with input: String) -> ResponseBodyDTO + func fetchPlaceList(with input: String) async throws -> ResponseBodyDTO? +} + +extension UtilService: FindPlaceServiceType { + func fetchPlaceList(with input: String) async throws-> ResponseBodyDTO? { + return try await request(with: .searchPlaceList(query: input)) + } } final class MockFindPlaceService: FindPlaceServiceType { - func fetchPlaceList(with input: String) -> ResponseBodyDTO { + func fetchPlaceList(with input: String) async throws -> ResponseBodyDTO? { let mockData: PlaceModel = PlaceModel( places: [ Place( diff --git a/KkuMulKum/Source/AddPromise/ServiceType/SelectMemeberServiceType.swift b/KkuMulKum/Source/AddPromise/ServiceType/SelectMemeberServiceType.swift index 91e0df17..2218b14f 100644 --- a/KkuMulKum/Source/AddPromise/ServiceType/SelectMemeberServiceType.swift +++ b/KkuMulKum/Source/AddPromise/ServiceType/SelectMemeberServiceType.swift @@ -8,11 +8,17 @@ import Foundation protocol SelectMemeberServiceType { - func fetchMeetingMemberList(with meetingID: Int) -> ResponseBodyDTO + func fetchMeetingMemberList( + with meetingID: Int + ) async throws -> ResponseBodyDTO? } +extension MeetingService: SelectMemeberServiceType {} + final class MockSelectMemberService: SelectMemeberServiceType { - func fetchMeetingMemberList(with meetingID: Int) -> ResponseBodyDTO { + func fetchMeetingMemberList( + with meetingID: Int + ) async throws -> ResponseBodyDTO? { let mockData = MeetingMembersModel( memberCount: 14, members: [ diff --git a/KkuMulKum/Source/AddPromise/ServiceType/SelectPenaltyService.swift b/KkuMulKum/Source/AddPromise/ServiceType/SelectPenaltyService.swift index cf91b8d3..b090422b 100644 --- a/KkuMulKum/Source/AddPromise/ServiceType/SelectPenaltyService.swift +++ b/KkuMulKum/Source/AddPromise/ServiceType/SelectPenaltyService.swift @@ -12,14 +12,37 @@ protocol SelectPenaltyServiceType { func requestAddingNewPromise( with requestModel: AddPromiseRequestModel, meetingID: Int - ) -> ResponseBodyDTO + ) async throws -> ResponseBodyDTO? +} + +extension PromiseService: SelectPenaltyServiceType { + func requestAddingNewPromise( + with requestModel: AddPromiseRequestModel, + meetingID: Int + ) async throws -> ResponseBodyDTO? { + return try await request( + with: .addPromise( + meetingID: meetingID, + request: requestModel + ) + ) + } } final class MockSelectPenaltyService: SelectPenaltyServiceType { func requestAddingNewPromise( with requestModel: AddPromiseRequestModel, meetingID: Int - ) -> ResponseBodyDTO { - return ResponseBodyDTO(success: true, data: nil, error: nil) + ) async throws -> ResponseBodyDTO? { + let mockData = AddPromiseResponseModel( + promiseID: 1, + placeName: "홍대입구", + address: "주소", + roadAddress: "도로명주소", + time: "", + dressUpLevel: "", + penalty: "" + ) + return ResponseBodyDTO(success: true, data: mockData, error: nil) } } diff --git a/KkuMulKum/Source/AddPromise/View/FindPlaceView.swift b/KkuMulKum/Source/AddPromise/View/FindPlaceView.swift index 94aec2f6..9314d497 100644 --- a/KkuMulKum/Source/AddPromise/View/FindPlaceView.swift +++ b/KkuMulKum/Source/AddPromise/View/FindPlaceView.swift @@ -61,7 +61,7 @@ final class FindPlaceView: BaseView { } extension FindPlaceView { - var placeTextFieldDidChange: Observable { placeTextField.rx.text.asObservable() } + var placeTextFieldDidChange: Observable { placeTextField.rx.text.orEmpty.asObservable() } var confirmButtonDidTap: Observable { confirmButton.rx.tap.asObservable() } func configureTextField(flag: Bool) { diff --git a/KkuMulKum/Source/AddPromise/ViewController/AddPromiseViewController.swift b/KkuMulKum/Source/AddPromise/ViewController/AddPromiseViewController.swift index a0dbb090..252c29a3 100644 --- a/KkuMulKum/Source/AddPromise/ViewController/AddPromiseViewController.swift +++ b/KkuMulKum/Source/AddPromise/ViewController/AddPromiseViewController.swift @@ -86,7 +86,7 @@ final class AddPromiseViewController: BaseViewController { .subscribe(with: self) { owner, _ in let viewController = FindPlaceViewController( viewModel: FindPlaceViewModel( - service: MockFindPlaceService() + service: UtilService() ) ) viewController.delegate = owner diff --git a/KkuMulKum/Source/AddPromise/ViewModel/FindPlaceViewModel.swift b/KkuMulKum/Source/AddPromise/ViewModel/FindPlaceViewModel.swift index 7e136ce0..c726fa89 100644 --- a/KkuMulKum/Source/AddPromise/ViewModel/FindPlaceViewModel.swift +++ b/KkuMulKum/Source/AddPromise/ViewModel/FindPlaceViewModel.swift @@ -23,7 +23,7 @@ final class FindPlaceViewModel { extension FindPlaceViewModel: ViewModelType { struct Input { - let textFieldDidChange: Observable + let textFieldDidChange: Observable let textFieldEndEditing: PublishRelay let cellIsSelected: PublishRelay let confirmButtonDidTap: Observable @@ -43,17 +43,9 @@ extension FindPlaceViewModel: ViewModelType { input.textFieldEndEditing .withLatestFrom(input.textFieldDidChange) - .map { [weak self] text -> [Place] in - guard let text, - !text.isEmpty, - let responseBodyDTO = self?.service.fetchPlaceList(with: text), - let data = responseBodyDTO.data - else { - return [] - } - return data.places + .subscribe(with: self) { owner, text in + owner.fetchPlaceList(with: text) } - .bind(to: placeListRelay) .disposed(by: disposeBag) let placeList = placeListRelay @@ -85,3 +77,22 @@ extension FindPlaceViewModel: ViewModelType { return output } } + +private extension FindPlaceViewModel { + func fetchPlaceList(with input: String) { + Task { + do { + guard let responseBody = try await self.service.fetchPlaceList(with: input), + responseBody.success, + let places = responseBody.data?.places + else { + placeListRelay.accept([]) + return + } + placeListRelay.accept(places) + } catch { + print(">>> \(error.localizedDescription) : \(#function)") + } + } + } +} diff --git a/KkuMulKum/Source/AddPromise/ViewModel/SelectMemberViewModel.swift b/KkuMulKum/Source/AddPromise/ViewModel/SelectMemberViewModel.swift index 8f5ff62f..cd2a7c72 100644 --- a/KkuMulKum/Source/AddPromise/ViewModel/SelectMemberViewModel.swift +++ b/KkuMulKum/Source/AddPromise/ViewModel/SelectMemberViewModel.swift @@ -52,13 +52,9 @@ extension SelectMemberViewModel: ViewModelType { func transform(input: Input, disposeBag: DisposeBag) -> Output { input.viewDidLoad - .map { [weak self] _ -> [Member] in - guard let self else { return [] } - let responseBodyDTO = service.fetchMeetingMemberList(with: meetingID) - guard let data = responseBodyDTO.data else { return [] } - return data.members + .subscribe(with: self) { owner, _ in + owner.fetchMeetingMembers() } - .bind(to: memberListRelay) .disposed(by: disposeBag) input.memberSelected @@ -69,7 +65,7 @@ extension SelectMemberViewModel: ViewModelType { owner.selectedMemberListRelay.accept(selectedMembers) } .disposed(by: disposeBag) - + input.memberDeselected .subscribe(with: self) { owner, member in var selectedMembers = owner.selectedMemberListRelay.value @@ -91,3 +87,21 @@ extension SelectMemberViewModel: ViewModelType { return output } } + +private extension SelectMemberViewModel { + func fetchMeetingMembers() { + Task { + do { + guard let responseBody = try await service.fetchMeetingMemberList(with: meetingID), + responseBody.success + else { + memberListRelay.accept([]) + return + } + memberListRelay.accept(responseBody.data?.members ?? []) + } catch { + print(">>> \(error.localizedDescription) : \(#function)") + } + } + } +} diff --git a/KkuMulKum/Source/AddPromise/ViewModel/SelectPenaltyViewModel.swift b/KkuMulKum/Source/AddPromise/ViewModel/SelectPenaltyViewModel.swift index 8165b6d9..dda93e25 100644 --- a/KkuMulKum/Source/AddPromise/ViewModel/SelectPenaltyViewModel.swift +++ b/KkuMulKum/Source/AddPromise/ViewModel/SelectPenaltyViewModel.swift @@ -20,6 +20,7 @@ final class SelectPenaltyViewModel { private let service: SelectPenaltyServiceType private let levelRelay = BehaviorRelay(value: "") private let penaltyRelay = BehaviorRelay(value: "") + private let newPromiseRelay = BehaviorRelay(value: nil) init( meetingID: Int, @@ -62,15 +63,19 @@ extension SelectPenaltyViewModel: ViewModelType { input.selectedPenaltyButton .bind(to: penaltyRelay) .disposed(by: disposeBag) + + input.confirmButtonDidTap + .subscribe(with: self) { owner, _ in + owner.requestAddNewPromise() + } + .disposed(by: disposeBag) - let isSucceedToCreate = input.confirmButtonDidTap - .map { [weak self] _ -> (Bool, Int?) in - guard let self else { return (false, nil) } - let result = service.requestAddingNewPromise( - with: createAddPromiseModel(), - meetingID: meetingID - ) - return (result.success, result.data?.promiseID) + let isSucceedToCreate = newPromiseRelay + .map { promise -> (Bool, Int?) in + guard let promise else { + return (false, nil) + } + return (true, promise.promiseID) } .asDriver(onErrorJustReturn: (false, nil)) @@ -101,4 +106,23 @@ private extension SelectPenaltyViewModel { return addPromiseModel } + + func requestAddNewPromise() { + Task { + do { + guard let responseBody = try await service.requestAddingNewPromise( + with: createAddPromiseModel(), + meetingID: meetingID + ), + responseBody.success + else { + newPromiseRelay.accept(nil) + return + } + newPromiseRelay.accept(responseBody.data) + } catch { + print(">>> \(error.localizedDescription) : \(#function)") + } + } + } } diff --git a/KkuMulKum/Source/Home/View/HomeView.swift b/KkuMulKum/Source/Home/View/HomeView.swift index 7d68da24..38db8bfa 100644 --- a/KkuMulKum/Source/Home/View/HomeView.swift +++ b/KkuMulKum/Source/Home/View/HomeView.swift @@ -64,7 +64,7 @@ final class HomeView: BaseView { $0.setText("다가올 나의 약속은?", style: .body01, color: .gray8) } - private let todayButton = UIButton().then { + let todayButton = UIButton().then { let icon = UIImage(resource: .iconRight) $0.setImage(icon, for: .normal) } diff --git a/KkuMulKum/Source/Home/ViewController/HomeViewController.swift b/KkuMulKum/Source/Home/ViewController/HomeViewController.swift index c1bdd3a9..66b4e0b6 100644 --- a/KkuMulKum/Source/Home/ViewController/HomeViewController.swift +++ b/KkuMulKum/Source/Home/ViewController/HomeViewController.swift @@ -65,6 +65,11 @@ class HomeViewController: BaseViewController { } override func setupAction() { + rootView.todayButton.addTarget( + self, + action: #selector(todayButtonDidTap), + for: .touchUpInside + ) rootView.todayPromiseView.prepareButton.addTarget( self, action: #selector(prepareButtonDidTap), @@ -116,6 +121,24 @@ extension HomeViewController: UICollectionViewDelegateFlowLayout { ) -> UIEdgeInsets { return contentInset } + + func collectionView( + _ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath + ) { + // TODO: promiseID를 모임 상세로 전달 + print( + "promiseID: ", + viewModel.upcomingPromiseList.value?.data?.promises[indexPath.item].promiseID ?? 0 + ) +// let viewController = PromiseInfoViewController( +// viewModel: PromiseInfoViewModel( +// promiseID: viewModel.upcomingPromiseList.value?.data?.promises[indexPath.item].promiseID ?? 0, +// service: PromiseService() +// ) +// ) + //tabBarController?.navigationController?.pushViewController(viewController, animated: true) + } } @@ -354,6 +377,22 @@ private extension HomeViewController { // MARK: - Action + @objc + func todayButtonDidTap(_ sender: UIButton) { + print( + "promiseID: ", + viewModel.nearestPromise.value?.data?.promiseID ?? 0 + ) + // TODO: promiseID를 모임 상세로 전달 +// let viewController = PromiseInfoViewController( +// viewModel: PromiseInfoViewModel( +// promiseID: viewModel.nearestPromise.value?.data?.promiseID ?? 0, +// service: PromiseService() +// ) +// ) + //tabBarController?.navigationController?.pushViewController(viewController, animated: true) + } + @objc func prepareButtonDidTap(_ sender: UIButton) { viewModel.updateState(newState: .prepare) diff --git a/KkuMulKum/Source/MeetingInfo/Cell/MeetingPromiseCell.swift b/KkuMulKum/Source/MeetingInfo/Cell/MeetingPromiseCell.swift index 470ada19..b5098c6d 100644 --- a/KkuMulKum/Source/MeetingInfo/Cell/MeetingPromiseCell.swift +++ b/KkuMulKum/Source/MeetingInfo/Cell/MeetingPromiseCell.swift @@ -93,7 +93,7 @@ final class MeetingPromiseCell: BaseCollectionViewCell { extension MeetingPromiseCell { func configure(dDay: Int, name: String, date: String, time: String, place: String) { - let dDayText = dDay == 0 ? "day" : "\(dDay)" + let dDayText = dDay == 0 ? "Day" : "\(dDay)" dDayLabel.setText("D-\(dDayText)", style: .body05, color: dDay == 0 ? .mainorange : .gray5) nameLabel.setText(name, style: .body03, color: .gray8) dateLabel.setText(date, style: .body06, color: .gray7) diff --git a/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift b/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift index 5961163f..06ac248c 100644 --- a/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift +++ b/KkuMulKum/Source/MeetingInfo/ViewController/MeetingInfoViewController.swift @@ -65,6 +65,18 @@ final class MeetingInfoViewController: BaseViewController { override func setupView() { setupNavigationBarBackButton() } + + override func setupDelegate() { + rootView.promiseListView.delegate = self + } +} + +extension MeetingInfoViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + // TODO: promiseID를 모임 상세로 전달 + /// viewModel.meetingPromises[indexPath.item]을 전달 + print(">>> \(viewModel.meetingPromises[indexPath.item]) : \(#function)") + } } private extension MeetingInfoViewController { diff --git a/KkuMulKum/Source/MeetingList/ServiceType/MeetingListServiceType.swift b/KkuMulKum/Source/MeetingList/ServiceType/MeetingListServiceType.swift index 60f444c7..d60cfa1c 100644 --- a/KkuMulKum/Source/MeetingList/ServiceType/MeetingListServiceType.swift +++ b/KkuMulKum/Source/MeetingList/ServiceType/MeetingListServiceType.swift @@ -13,6 +13,12 @@ protocol MeetingListServiceType { func fetchMeetingList() -> ResponseBodyDTO } +//extension MeetingService: MeetingListServiceType { +// func fetchMeetingList() -> ResponseBodyDTO { +// <#code#> +// } +//} + final class MockMeetingListService: MeetingListServiceType { func fetchMeetingList() -> ResponseBodyDTO { let mockData = ResponseBodyDTO( diff --git a/KkuMulKum/Source/MeetingList/ViewController/MeetingListViewController.swift b/KkuMulKum/Source/MeetingList/ViewController/MeetingListViewController.swift index 43a343e0..f632d12e 100644 --- a/KkuMulKum/Source/MeetingList/ViewController/MeetingListViewController.swift +++ b/KkuMulKum/Source/MeetingList/ViewController/MeetingListViewController.swift @@ -85,10 +85,9 @@ extension MeetingListViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // TODO: MeetingID를 넘겨받기 let viewController = MeetingInfoViewController( viewModel: MeetingInfoViewModel( - meetingID: 8, + meetingID: viewModel.meetingList.value?.data?.meetings[indexPath.item].meetingID ?? 0, service: MeetingService() ) ) diff --git a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift index 5c80efc1..0e8fafc6 100644 --- a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift +++ b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift @@ -59,7 +59,7 @@ class LoginViewController: BaseViewController { print("Login State: Not logged in") case .login: print("Login State: Logged in with user info: ") - owner.navigateToMainScreen() + owner.navigateToOnboardingScreen() case .needOnboarding: print("Login State: Need onboarding") owner.navigateToOnboardingScreen() diff --git a/KkuMulKum/Source/Onboarding/Nickname/NicknameView/NicknameView.swift b/KkuMulKum/Source/Onboarding/Nickname/View/NicknameView.swift similarity index 100% rename from KkuMulKum/Source/Onboarding/Nickname/NicknameView/NicknameView.swift rename to KkuMulKum/Source/Onboarding/Nickname/View/NicknameView.swift diff --git a/KkuMulKum/Source/Onboarding/Nickname/ViewController/NicknameViewController.swift b/KkuMulKum/Source/Onboarding/Nickname/ViewController/NicknameViewController.swift index 349ddd4f..fe075a48 100644 --- a/KkuMulKum/Source/Onboarding/Nickname/ViewController/NicknameViewController.swift +++ b/KkuMulKum/Source/Onboarding/Nickname/ViewController/NicknameViewController.swift @@ -4,14 +4,26 @@ // // Created by 이지훈 on 7/10/24. // -// NicknameViewController.swift import UIKit +import RxSwift +import RxCocoa + class NicknameViewController: BaseViewController { private let nicknameView = NicknameView() - private let viewModel = NicknameViewModel() + private let viewModel: NicknameViewModel + private let disposeBag = DisposeBag() + + init(viewModel: NicknameViewModel = NicknameViewModel()) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func loadView() { view = nicknameView @@ -23,63 +35,90 @@ class NicknameViewController: BaseViewController { setupTextField() setupTapGesture() setupNavigationBarTitle(with: "닉네임 설정") - - print("---") - let keychainService = DefaultKeychainService.shared - - if let accessToken = keychainService.accessToken { - print("Access token is available in NicknameViewController: \(accessToken)") - } else { - print("No access token available in NicknameViewController. User may need to log in.") - } - } - - override func setupAction() { - nicknameView.nicknameTextField.addTarget( - self, - action: #selector(textFieldDidChange(_:)), - for: .editingChanged - ) - nicknameView.nextButton.addTarget( - self, - action: #selector(nextButtonTapped), - for: .touchUpInside - ) } private func setupBindings() { - viewModel.nicknameState.bind { [weak self] state in - switch state { - case .empty: - self?.nicknameView.nicknameTextField.layer.borderColor = UIColor.gray3.cgColor - self?.nicknameView.errorLabel.isHidden = true - case .valid: - self?.nicknameView.nicknameTextField.layer.borderColor = UIColor.maincolor.cgColor - self?.nicknameView.errorLabel.isHidden = true - case .invalid: - self?.nicknameView.nicknameTextField.layer.borderColor = UIColor.red.cgColor - self?.nicknameView.errorLabel.isHidden = false - } - } + nicknameView.nicknameTextField.rx.text.orEmpty + .bind(to: viewModel.nicknameText) + .disposed(by: disposeBag) + + viewModel.nicknameState + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] state in + self?.updateUIForNicknameState(state) + }) + .disposed(by: disposeBag) + + viewModel.errorMessage + .bind(to: nicknameView.errorLabel.rx.text) + .disposed(by: disposeBag) + + viewModel.isNextButtonEnabled + .bind(to: nicknameView.nextButton.rx.isEnabled) + .disposed(by: disposeBag) + + viewModel.isNextButtonValid + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isValid in + self?.updateNextButtonAppearance(isValid: isValid) + }) + .disposed(by: disposeBag) - viewModel.errorMessage.bind { [weak self] errorMessage in - self?.nicknameView.errorLabel.text = errorMessage - } + viewModel.characterCount + .bind(to: nicknameView.characterCountLabel.rx.text) + .disposed(by: disposeBag) - viewModel.isNextButtonEnabled.bind { [weak self] isEnabled in - self?.nicknameView.nextButton.isEnabled = isEnabled - self?.nicknameView.nextButton.backgroundColor = isEnabled ? .maincolor : .gray2 - } + nicknameView.nextButton.rx.tap + .bind(to: viewModel.updateNicknameTrigger) + .disposed(by: disposeBag) - viewModel.characterCount.bind { [weak self] count in - self?.nicknameView.characterCountLabel.text = count - } + viewModel.nicknameUpdateSuccess + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] nickname in + self?.navigateToProfileSetup(with: nickname) + }) + .disposed(by: disposeBag) - viewModel.serverResponse.bind { response in - if let response = response { - print("서버 응답: \(response)") - } - } + viewModel.serverResponse + .observe(on: MainScheduler.instance) + .subscribe(onNext: { response in + print("서버 응답: \(response ?? "")") + }) + .disposed(by: disposeBag) + + nicknameView.nicknameTextField.rx.controlEvent(.editingDidEnd) + .withLatestFrom(viewModel.nicknameState) + .subscribe(onNext: { [weak self] state in + self?.updateUIForNicknameState(state) + }) + .disposed(by: disposeBag) + } + + private func updateUIForNicknameState(_ state: NicknameState) { + switch state { + case .empty: + nicknameView.nicknameTextField.layer.borderColor = UIColor.gray3.cgColor + nicknameView.errorLabel.isHidden = true + case .valid: + nicknameView.nicknameTextField.layer.borderColor = UIColor.maincolor.cgColor + nicknameView.errorLabel.isHidden = true + case .invalid: + nicknameView.nicknameTextField.layer.borderColor = UIColor.red.cgColor + nicknameView.errorLabel.isHidden = false + } + updateNextButtonAppearance(isValid: state == .valid) + } + + private func updateNextButtonAppearance(isValid: Bool) { + nicknameView.nextButton.backgroundColor = isValid ? .maincolor : .gray2 + nicknameView.nextButton.isEnabled = isValid + } + + + private func navigateToProfileSetup(with nickname: String) { + let profileSetupVM = ProfileSetupViewModel(nickname: nickname) + let profileSetupVC = ProfileSetupViewController(viewModel: profileSetupVM) + navigationController?.pushViewController(profileSetupVC, animated: true) } private func setupTextField() { @@ -88,40 +127,21 @@ class NicknameViewController: BaseViewController { } private func setupTapGesture() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + let tapGesture = UITapGestureRecognizer() + tapGesture.rx.event + .subscribe(onNext: { [weak self] _ in + self?.view.endEditing(true) + self?.updateUIForNicknameState(self?.viewModel.nicknameState.value ?? .empty) + }) + .disposed(by: disposeBag) view.addGestureRecognizer(tapGesture) } - - @objc private func textFieldDidChange(_ textField: UITextField) { - viewModel.validateNickname(textField.text ?? "") - } - - @objc private func nextButtonTapped() { - viewModel.updateNickname { [weak self] success in - if success { - print("닉네임이 성공적으로 서버에 등록되었습니다.") - let profileSetupVC = ProfileSetupViewController( - viewModel: ProfileSetupViewModel( - nickname: self?.viewModel.nickname.value ?? "" - ) - ) - self?.navigationController?.pushViewController(profileSetupVC, animated: true) - } else { - print("닉네임 등록에 실패했습니다.") - } - } - } - - @objc private func dismissKeyboard() { - view.endEditing(true) - nicknameView.nicknameTextField.layer.borderColor = UIColor.gray3.cgColor - } } extension NicknameViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() - nicknameView.nicknameTextField.layer.borderColor = UIColor.gray3.cgColor + updateUIForNicknameState(viewModel.nicknameState.value) return true } } diff --git a/KkuMulKum/Source/Onboarding/Nickname/ViewModel/NicknameViewModel.swift b/KkuMulKum/Source/Onboarding/Nickname/ViewModel/NicknameViewModel.swift index d1b90e05..1f8ed3f1 100644 --- a/KkuMulKum/Source/Onboarding/Nickname/ViewModel/NicknameViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Nickname/ViewModel/NicknameViewModel.swift @@ -7,6 +7,8 @@ import Foundation +import RxSwift +import RxCocoa import Moya enum NicknameState { @@ -16,98 +18,95 @@ enum NicknameState { } class NicknameViewModel { - let nickname = ObservablePattern("") - let nicknameState = ObservablePattern(.empty) - let errorMessage = ObservablePattern(nil) - let isNextButtonEnabled = ObservablePattern(false) - let characterCount = ObservablePattern("0/5") + let nicknameText = BehaviorRelay(value: "") + let updateNicknameTrigger = PublishSubject() - private let nicknameRegex = "^[가-힣a-zA-Z0-9]{1,5}$" - private let provider = MoyaProvider() - let serverResponse = ObservablePattern(nil) + let nicknameState: BehaviorRelay + let errorMessage: Observable + let isNextButtonEnabled: Observable + let isNextButtonValid: Observable + let characterCount: Observable + let serverResponse = PublishSubject() + let nicknameUpdateSuccess = PublishSubject() + private let disposeBag = DisposeBag() + private let nicknameRegex = "^[가-힣a-zA-Z]{1,5}$" + private let provider: MoyaProvider private let authService: AuthServiceType - init(authService: AuthServiceType = AuthService()) { + init(provider: MoyaProvider = MoyaProvider(), + authService: AuthServiceType = AuthService()) { + self.provider = provider self.authService = authService - } - - func validateNickname(_ name: String) { - nickname.value = name - characterCount.value = "\(name.count)/5" - - if name.isEmpty { - nicknameState.value = .empty - errorMessage.value = nil - isNextButtonEnabled.value = false - } else if name.range(of: nicknameRegex, options: .regularExpression) != nil { - nicknameState.value = .valid - errorMessage.value = nil - isNextButtonEnabled.value = true - } else { - nicknameState.value = .invalid - errorMessage.value = "한글, 영문, 숫자만을 사용해 총 5자 이내로 입력해주세요." - isNextButtonEnabled.value = false - } - } - - func updateNickname(completion: @escaping (Bool) -> Void) { - guard nicknameState.value == .valid else { - completion(false) - return - } - - guard let accessToken = authService.getAccessToken() else { - print("No access token available") - completion(false) - return - } - print("닉네임 업데이트 요청 시작: \(nickname.value)") + nicknameState = BehaviorRelay(value: .empty) - let headers = [ - "Content-Type": "application/json", - "Authorization": "Bearer \(accessToken)" - ] - - print("요청 헤더: \(headers)") - print("요청 바디: \(["name": nickname.value])") + errorMessage = nicknameState + .map { state in + state == .invalid ? "한글, 영문만을 사용해 총 5자 이내로 입력해주세요." : nil + } + isNextButtonEnabled = nicknameState.map { $0 == .valid } + isNextButtonValid = nicknameState.map { $0 == .valid } + + characterCount = nicknameText.map { "\($0.count)/5" } - provider.request(.updateName(name: nickname.value)) { [weak self] result in - switch result { - case .success(let response): - print("서버 응답: \(String(data: response.data, encoding: .utf8) ?? "Unable to decode response")") - do { - let decodedResponse = try JSONDecoder().decode(ResponseBodyDTO.self, from: response.data) - if decodedResponse.success { - self?.serverResponse.value = "닉네임이 성공적으로 업데이트되었습니다." - print("닉네임 업데이트 성공: \(self?.nickname.value ?? "")") - completion(true) + setupBindings() + } + + private func setupBindings() { + nicknameText + .map { [weak self] text in + guard let self = self else { return .empty } + if text.isEmpty { return .empty } + return text.range(of: self.nicknameRegex, options: .regularExpression) != nil ? .valid : .invalid + } + .bind(to: nicknameState) + .disposed(by: disposeBag) + + updateNicknameTrigger + .withLatestFrom(nicknameText) + .flatMapLatest { [weak self] nickname -> Observable, Error>> in + guard let self = self else { return .empty() } + return self.updateNickname(nickname: nickname) + } + .subscribe(onNext: { [weak self] result in + switch result { + case .success(let response): + if response.success { + self?.serverResponse.onNext("닉네임이 성공적으로 업데이트되었습니다.") + self?.nicknameUpdateSuccess.onNext(self?.nicknameText.value ?? "") } else { - if let errorCode = decodedResponse.error?.code { - switch errorCode { - case 40420: - self?.serverResponse.value = "사용자를 찾을 수 없습니다." - default: - self?.serverResponse.value = decodedResponse.error?.message ?? "알 수 없는 오류가 발생했습니다." - } - } else { - self?.serverResponse.value = decodedResponse.error?.message ?? "알 수 없는 오류가 발생했습니다." - } - print("닉네임 업데이트 실패: \(self?.serverResponse.value ?? "알 수 없는 오류")") - completion(false) + self?.serverResponse.onNext(response.error?.message ?? "알 수 없는 오류가 발생했습니다.") } - } catch { - self?.serverResponse.value = "데이터 디코딩 중 오류가 발생했습니다." - print("닉네임 업데이트 실패: 데이터 디코딩 오류 - \(error)") - completion(false) + case .failure(let error): + self?.serverResponse.onNext("네트워크 오류: \(error.localizedDescription)") } - case .failure(let error): - self?.serverResponse.value = "네트워크 오류: \(error.localizedDescription)" - print("닉네임 업데이트 실패: 네트워크 오류 - \(error.localizedDescription)") - completion(false) + }) + .disposed(by: disposeBag) + } + + private func updateNickname(nickname: String) -> Observable, Error>> { + return Observable.create { [weak self] observer in + guard let self = self, let accessToken = self.authService.getAccessToken() else { + observer.onNext(.failure(NSError(domain: "No access token available", code: 0, userInfo: nil))) + observer.onCompleted() + return Disposables.create() } + self.provider.request(.updateName(name: nickname)) { result in + switch result { + case .success(let response): + do { + let decodedResponse = try JSONDecoder().decode(ResponseBodyDTO.self, from: response.data) + observer.onNext(.success(decodedResponse)) + } catch { + observer.onNext(.failure(error)) + } + case .failure(let error): + observer.onNext(.failure(error)) + } + observer.onCompleted() + } + return Disposables.create() } } - } diff --git a/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift b/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift index d4debc42..46970da6 100644 --- a/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift +++ b/KkuMulKum/Source/Onboarding/Profile/VIewController/ProfileSetupViewController.swift @@ -7,9 +7,13 @@ import UIKit +import RxSwift +import RxCocoa + class ProfileSetupViewController: BaseViewController { private let rootView = ProfileSetupView() private let viewModel: ProfileSetupViewModel + private let disposeBag = DisposeBag() init(viewModel: ProfileSetupViewModel) { self.viewModel = viewModel @@ -26,52 +30,52 @@ class ProfileSetupViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - setupNavigationBarTitle(with: "프로필 설정") setupNavigationBarBackButton() setupBindings() } - override func setupAction() { - rootView.confirmButton.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) - rootView.skipButton.addTarget(self, action: #selector(skipButtonTapped), for: .touchUpInside) - rootView.cameraButton.addTarget(self, action: #selector(cameraButtonTapped), for: .touchUpInside) - } - private func setupBindings() { - viewModel.profileImage.bind { [weak self] image in - self?.rootView.profileImageView.image = image - } + viewModel.profileImage + .bind(to: rootView.profileImageView.rx.image) + .disposed(by: disposeBag) - viewModel.isConfirmButtonEnabled.bind { [weak self] isEnabled in - self?.rootView.confirmButton.isEnabled = isEnabled - self?.rootView.confirmButton.alpha = isEnabled ? 1.0 : 0.5 - } - } - - @objc private func confirmButtonTapped() { - viewModel.uploadProfileImage { [weak self] success in - if success { - DispatchQueue.main.async { - let welcomeVC = WelcomeViewController( - viewModel: WelcomeViewModel(nickname: self?.viewModel.nickname ?? "") - ) - welcomeVC.modalPresentationStyle = .fullScreen - self?.present(welcomeVC, animated: true, completion: nil) - } - } - } - } - - @objc private func skipButtonTapped() { - let welcomeVC = WelcomeViewController( - viewModel: WelcomeViewModel(nickname: viewModel.nickname) - ) - welcomeVC.modalPresentationStyle = .fullScreen - present(welcomeVC, animated: true, completion: nil) + viewModel.isConfirmButtonEnabled + .bind(to: rootView.confirmButton.rx.isEnabled) + .disposed(by: disposeBag) + + viewModel.isConfirmButtonEnabled + .map { $0 ? 1.0 : 0.5 } + .bind(to: rootView.confirmButton.rx.alpha) + .disposed(by: disposeBag) + + rootView.confirmButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.viewModel.uploadProfileImage() + }) + .disposed(by: disposeBag) + + rootView.skipButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.navigateToWelcome() + }) + .disposed(by: disposeBag) + + rootView.cameraButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.showImagePicker() + }) + .disposed(by: disposeBag) + + viewModel.uploadSuccess + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + self?.navigateToWelcome() + }) + .disposed(by: disposeBag) } - @objc private func cameraButtonTapped() { + private func showImagePicker() { let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.sourceType = .photoLibrary @@ -79,25 +83,16 @@ class ProfileSetupViewController: BaseViewController { present(imagePicker, animated: true) } - private func cropToCircle(image: UIImage) -> UIImage { - let shorterSide = min(image.size.width, image.size.height) - let imageBounds = CGRect(x: 0, y: 0, width: shorterSide, height: shorterSide) - UIGraphicsBeginImageContextWithOptions(imageBounds.size, false, UIScreen.main.scale) - let context = UIGraphicsGetCurrentContext()! - context.addEllipse(in: imageBounds) - context.clip() - image.draw(in: imageBounds) - let circleImage = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - return circleImage + private func navigateToWelcome() { + let welcomeVM = WelcomeViewModel(nickname: viewModel.nickname) + let welcomeVC = WelcomeViewController(viewModel: welcomeVM) + welcomeVC.modalPresentationStyle = .fullScreen + present(welcomeVC, animated: true, completion: nil) } } - + extension ProfileSetupViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - func imagePickerController( - _ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] - ) { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let editedImage = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage { let croppedImage = cropToCircle(image: editedImage) viewModel.updateProfileImage(croppedImage) @@ -108,4 +103,18 @@ extension ProfileSetupViewController: UIImagePickerControllerDelegate, UINavigat func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true) } + + private func cropToCircle(image: UIImage) -> UIImage { + let shorterSide = min(image.size.width, image.size.height) + let imageBounds = CGRect(x: 0, y: 0, width: shorterSide, height: shorterSide) + UIGraphicsBeginImageContextWithOptions(imageBounds.size, false, UIScreen.main.scale) + let context = UIGraphicsGetCurrentContext()! + context.addEllipse(in: imageBounds) + context.clip() + image.draw(in: imageBounds) + let circleImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return circleImage + } } + diff --git a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift index c225d98b..e11b38c3 100644 --- a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift @@ -8,13 +8,23 @@ import UIKit +import RxSwift +import RxCocoa +import Moya + class ProfileSetupViewModel { - let profileImage = ObservablePattern(UIImage.imgProfile) - let isConfirmButtonEnabled = ObservablePattern(false) - let nickname: String - let serverResponse = ObservablePattern(nil) + // Inputs + let updateProfileImageTrigger = PublishSubject() - private let authService: AuthServiceType + // Outputs + let profileImage = BehaviorRelay(value: UIImage.imgProfile) + let isConfirmButtonEnabled = BehaviorRelay(value: false) + let serverResponse = PublishSubject() + let uploadSuccess = PublishSubject() + + let nickname: String + private let disposeBag = DisposeBag() + private let provider = MoyaProvider() init(nickname: String, authService: AuthServiceType = AuthService()) { self.nickname = nickname @@ -41,18 +51,65 @@ class ProfileSetupViewModel { let fileName = "profile_image.jpg" let mimeType = "image/jpeg" - authService.performRequest(.updateProfileImage(image: imageData, fileName: fileName, mimeType: mimeType)) { [weak self] (result: Result) in - print("네트워크 요청 완료") - switch result { - case .success: - self?.serverResponse.value = "프로필 이미지가 성공적으로 업로드되었습니다." - print("프로필 이미지 업로드 성공") - completion(true) - case .failure(let error): - self?.serverResponse.value = error.message - print("프로필 이미지 업로드 실패: \(error.message)") - completion(false) + updateProfileImageTrigger + .withLatestFrom(profileImage) + .flatMapLatest { [weak self] image -> Observable, Error>> in + guard let self = self, let image = image else { return .empty() } + return self.uploadProfileImage(image: image) + } + .subscribe(onNext: { [weak self] result in + switch result { + case .success(let response): + if response.success { + self?.serverResponse.onNext("프로필 이미지가 성공적으로 업로드되었습니다.") + self?.uploadSuccess.onNext(()) + } else { + self?.serverResponse.onNext(response.error?.message ?? "알 수 없는 오류가 발생했습니다.") + } + case .failure(let error): + self?.serverResponse.onNext("네트워크 오류: \(error.localizedDescription)") + } + }) + .disposed(by: disposeBag) + } + + func updateProfileImage(_ image: UIImage?) { + profileImage.accept(image) + isConfirmButtonEnabled.accept(image != nil) + } + + func uploadProfileImage() { + updateProfileImageTrigger.onNext(()) + } + + private func uploadProfileImage(image: UIImage) -> Observable, Error>> { + return Observable.create { [weak self] observer in + guard let self = self, + let imageData = image.jpegData(compressionQuality: 0.8) else { + observer.onNext(.failure(NSError(domain: "Image conversion failed", code: 0, userInfo: nil))) + observer.onCompleted() + return Disposables.create() + } + + let fileName = "profile_image.jpg" + let mimeType = "image/jpeg" + + self.provider.request(.updateProfileImage(image: imageData, fileName: fileName, mimeType: mimeType)) { result in + switch result { + case .success(let response): + do { + let decodedResponse = try JSONDecoder().decode(ResponseBodyDTO.self, from: response.data) + observer.onNext(.success(decodedResponse)) + } catch { + observer.onNext(.failure(error)) + } + case .failure(let error): + observer.onNext(.failure(error)) + } + observer.onCompleted() } + + return Disposables.create() } } } diff --git a/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift b/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift index 9388d25d..970d6fa8 100644 --- a/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift +++ b/KkuMulKum/Source/Promise/PagePromise/ViewController/PagePromiseViewController.swift @@ -40,6 +40,23 @@ class PagePromiseViewController: BaseViewController { navigationOrientation: .vertical ) + override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationBarBackButton() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.isNavigationBarHidden = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + navigationController?.isNavigationBarHidden = true + } // MARK: - Setup diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift index 8e40a3c0..840b0a6c 100644 --- a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift +++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift @@ -29,6 +29,16 @@ final class SetReadyInfoViewModel { } } + private func calculateTimes() { + let readyHours = Int(readyHour.value) ?? 0 + let readyMinutes = Int(readyMinute.value) ?? 0 + let moveHours = Int(moveHour.value) ?? 0 + let moveMinutes = Int(moveMinute.value) ?? 0 + + readyTime = readyHours * 60 + readyMinutes + moveTime = moveHours * 60 + moveMinutes + } + func updateTime(textField: String, time: String) { guard let time = Int(time) else { return } @@ -44,6 +54,8 @@ final class SetReadyInfoViewModel { default: break } + + calculateTimes() } func checkValid(