diff --git a/iOS/Projects/App/WeTri/Sources/Application/SceneDelegate.swift b/iOS/Projects/App/WeTri/Sources/Application/SceneDelegate.swift index 34e07906..0ea8ecdb 100644 --- a/iOS/Projects/App/WeTri/Sources/Application/SceneDelegate.swift +++ b/iOS/Projects/App/WeTri/Sources/Application/SceneDelegate.swift @@ -20,14 +20,11 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = scene as? UIWindowScene else { return } window = UIWindow(windowScene: windowScene) let navigationController = UINavigationController() - let viewController = SignUpContainerViewController( - signUpGenderBirthViewController: SignUpGenderBirthViewController(viewModel: SignUpGenderBirthViewModel(dateFormatUseCase: DateFormatUseCase())), - signUpProfileViewController: SignUpProfileViewController() - ) - window?.rootViewController = viewController -// let coordinator = AppCoordinator(navigationController: navigationController) -// coordinating = coordinator -// coordinator.start() + window = UIWindow(windowScene: windowScene) + let coordinator = AppCoordinator(navigationController: navigationController) + coordinating = coordinator + coordinator.start() + window?.rootViewController = navigationController window?.makeKeyAndVisible() } } diff --git a/iOS/Projects/App/WeTri/Sources/CommonScene/Coordinator/AppCoordinator.swift b/iOS/Projects/App/WeTri/Sources/CommonScene/Coordinator/AppCoordinator.swift index 705e9c1c..09e005ca 100644 --- a/iOS/Projects/App/WeTri/Sources/CommonScene/Coordinator/AppCoordinator.swift +++ b/iOS/Projects/App/WeTri/Sources/CommonScene/Coordinator/AppCoordinator.swift @@ -25,7 +25,7 @@ final class AppCoordinator: AppCoordinating { } func start() { - showSplashFlow() + showTabBarFlow() } private func showSplashFlow() { diff --git a/iOS/Projects/Features/Record/Resources/Persistency/MatchesCancel.json b/iOS/Projects/Features/Record/Resources/Persistency/MatchesCancel.json index 4c78f90e..b336cc0e 100644 --- a/iOS/Projects/Features/Record/Resources/Persistency/MatchesCancel.json +++ b/iOS/Projects/Features/Record/Resources/Persistency/MatchesCancel.json @@ -1,5 +1,5 @@ { - "code": 200, + "code": null, "errorMessage": null, "data": null } diff --git a/iOS/Projects/Features/Record/Resources/Persistency/MatchesRandom.json b/iOS/Projects/Features/Record/Resources/Persistency/MatchesRandom.json index 2cf14813..7e3db432 100644 --- a/iOS/Projects/Features/Record/Resources/Persistency/MatchesRandom.json +++ b/iOS/Projects/Features/Record/Resources/Persistency/MatchesRandom.json @@ -1,5 +1,24 @@ { - "code": 201, + "code": null, "errorMessage": null, - "data": null + "data": { + "matched": true, + "liveWorkoutStartTime": "2023-12-05 16:33:00", + "roomId": "uuid", + "publicId": "someStringss", + "peers": [ + { + "nickname": "나는 1번 타자", + "publicId": "someString", + "profileImage": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Cat-eating-prey.jpg/220px-Cat-eating-prey.jpg", + "etc": "그 외 나머지 모든 컬럼" + }, + { + "nickname": "나는 2번타자", + "publicId": "someStringw", + "profileImage": "https://www.telegraph.co.uk/content/dam/pets/2017/01/06/1-JS117202740-yana-two-face-cat-news_trans_NvBQzQNjv4BqJNqHJA5DVIMqgv_1zKR2kxRY9bnFVTp4QZlQjJfe6H0.jpg?imwidth=450", + "etc": "그 외 나머지 모든 컬럼" + } + ] + } } diff --git a/iOS/Projects/Features/Record/Resources/Persistency/MatchesStart.json b/iOS/Projects/Features/Record/Resources/Persistency/MatchesStart.json index 4c78f90e..b336cc0e 100644 --- a/iOS/Projects/Features/Record/Resources/Persistency/MatchesStart.json +++ b/iOS/Projects/Features/Record/Resources/Persistency/MatchesStart.json @@ -1,5 +1,5 @@ { - "code": 200, + "code": null, "errorMessage": null, "data": null } diff --git a/iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersRequest.swift b/iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersRequest.swift new file mode 100644 index 00000000..b976b6cc --- /dev/null +++ b/iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersRequest.swift @@ -0,0 +1,24 @@ +// +// IsMatchedRandomPeersRequest.swift +// RecordFeature +// +// Created by MaraMincho on 12/5/23. +// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. +// + +import Foundation + +/// randomMatching API의 requst에 활용합니다. +/// +/// 어떤 운동과, 얼마나 randomMatching을 기다렸는지 알려주는 객체 입니다. +struct IsMatchedRandomPeersRequest: Encodable { + /// 어떤 운동을 랜덤 매칭 하는지 알려줍니다. + /// + /// 1번 : 달리기 + /// 2번 : 사이클 + /// 3번 : 수영 + let workoutID: Int + + /// 몇초를 대기방에서 기다렸는지 알려줍니다. + let waitingTime: Int +} diff --git a/iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersResponse.swift b/iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersResponse.swift new file mode 100644 index 00000000..c7a4b93b --- /dev/null +++ b/iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersResponse.swift @@ -0,0 +1,43 @@ +// +// IsMatchedRandomPeersResponse.swift +// RecordFeature +// +// Created by MaraMincho on 12/5/23. +// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. +// + +import Foundation + +// MARK: - IsMatchedRandomPeersResponse + +struct IsMatchedRandomPeersResponse: Decodable { + let matched: Bool + let liveWorkoutStartTime: String? + let roomID: String? + let publicID: String? + let peers: [IsMatchedRandomPeersForPeerResponse]? + + enum CodingKeys: String, CodingKey { + case matched + case liveWorkoutStartTime + case roomID = "roomId" + case publicID = "publicId" + case peers + } +} + +// MARK: - IsMatchedRandomPeersForPeerResponse + +struct IsMatchedRandomPeersForPeerResponse: Decodable { + let nickname: String + let publicID: String + let profileImage: String + let etc: String? + + enum CodingKeys: String, CodingKey { + case nickname + case publicID = "publicId" + case profileImage + case etc + } +} diff --git a/iOS/Projects/Features/Record/Sources/Data/DTO/MatchStartRequest.swift b/iOS/Projects/Features/Record/Sources/Data/DTO/MatchStartRequest.swift new file mode 100644 index 00000000..2381d7df --- /dev/null +++ b/iOS/Projects/Features/Record/Sources/Data/DTO/MatchStartRequest.swift @@ -0,0 +1,15 @@ +// +// MatchStartRequest.swift +// RecordFeature +// +// Created by MaraMincho on 12/5/23. +// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. +// + +import Foundation + +/// 매칭이 시작 될 떄 사용하는 Request Body입니다. +struct MatchStartRequest: Encodable { + /// workoutId는 운동 번호를 의미합니다. + let workoutID: Int +} diff --git a/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutEnvironmentSetupNetworkRepository.swift b/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutEnvironmentSetupNetworkRepository.swift index 0c9d1312..1a295682 100644 --- a/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutEnvironmentSetupNetworkRepository.swift +++ b/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutEnvironmentSetupNetworkRepository.swift @@ -78,16 +78,6 @@ private extension WorkoutEnvironmentSetupNetworkRepository { case exerciseTypes case peer - // TODO: API에 맞게 수정 예정 - var baseURL: String { - switch self { - case .exerciseTypes: - return "https://www.naver.com" - case .peer: - return "https://www.naver.com" - } - } - var path: String { switch self { case .exerciseTypes: @@ -100,7 +90,7 @@ private extension WorkoutEnvironmentSetupNetworkRepository { var method: TNMethod { return .get } var query: Encodable? { nil } var body: Encodable? { nil } - var headers: Trinet.TNHeaders { .init(headers: []) } + var headers: TNHeaders { .default } } enum PersistencyProperty { diff --git a/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutPeerRandomMatchingRepository.swift b/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutPeerRandomMatchingRepository.swift index 395ef774..9dbbd104 100644 --- a/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutPeerRandomMatchingRepository.swift +++ b/iOS/Projects/Features/Record/Sources/Data/Repositories/WorkoutPeerRandomMatchingRepository.swift @@ -7,6 +7,7 @@ // import Combine +import CommonNetworkingKeyManager import Foundation import Trinet @@ -27,15 +28,12 @@ extension WorkoutPeerRandomMatchingRepository: WorkoutPeerRandomMatchingReposito return Future, Never> { promise in Task { do { - let data = try await provider.request(.matchStart(workoutTypeCode: workoutTypeCode)) - let response = try decoder.decode(GWResponse.self, from: data) - // 200번대 REsponse인지 확인, 보통 서버에서 코드를 보내주지만, 안보내줄 경우 자동적으로 동작 안하게 작성) - if response.code == 200 { - promise(.success(.success(()))) - } else { - // TODO: ERROR Handling - promise(.success(.failure(RepositoryError.serverError))) - } + let data = try await provider.request( + .matchStart(matchStartRequest: .init(workoutID: workoutTypeCode)), + interceptor: TNKeychainInterceptor.shared + ) + _ = try decoder.decode(GWResponse.self, from: data) + promise(.success(.success(()))) } catch { promise(.success(.failure(error))) } @@ -53,21 +51,20 @@ extension WorkoutPeerRandomMatchingRepository: WorkoutPeerRandomMatchingReposito } } - func isMatchedRandomPeer(workoutTypeCode: Int) -> AnyPublisher, Never> { - return Future, Never> { promise in + func isMatchedRandomPeer( + isMatchedRandomPeersRequest: IsMatchedRandomPeersRequest + ) -> AnyPublisher, Never> { + return Future, Never> { promise in Task { do { - let data = try await provider.request(.isMatchedRandomPeer(workoutTypeCode: workoutTypeCode)) - let response = try decoder.decode(GWResponse.self, from: data) - if response.code == 201 { - promise(.success(.success(nil))) - } else if response.code == 200 { - promise(.success(.success(response.data))) - } else { - promise(.success(.failure(RepositoryError.serverError))) + let data = try await provider.request(.isMatchedRandomPeer(isMatchedRandomPeersRequest: isMatchedRandomPeersRequest)) + guard + let responseData = try decoder.decode(GWResponse.self, from: data).data + else { + throw RepositoryError.serverError } + promise(.success(.success(responseData))) } catch { - // TODO: ERROR Handling promise(.success(.failure(error))) } } @@ -86,20 +83,20 @@ enum RepositoryError: LocalizedError { extension WorkoutPeerRandomMatchingRepository { enum WorkoutPeerRandomMatchingRepositoryEndPoint: TNEndPoint { - /// Proeprty - case matchStart(workoutTypeCode: Int) + /// Property + case matchStart(matchStartRequest: MatchStartRequest) case matchCancel - case isMatchedRandomPeer(workoutTypeCode: Int) + case isMatchedRandomPeer(isMatchedRandomPeersRequest: IsMatchedRandomPeersRequest) /// TNEndPoint var path: String { switch self { case .matchStart: - return "matches/start" + return "api/v1/matches/start" case .matchCancel: - return "matches/cancle" + return "api/v1/matches/cancel" case .isMatchedRandomPeer: - return "matches/random" + return "api/v1/matches/random" } } @@ -109,7 +106,7 @@ extension WorkoutPeerRandomMatchingRepository { .matchStart: return .post case .matchCancel: - return .get + return .delete } } @@ -119,12 +116,9 @@ extension WorkoutPeerRandomMatchingRepository { var body: Encodable? { switch self { - case let .matchStart(workoutTypeCode): - return workoutTypeCode - case let .isMatchedRandomPeer(workoutTypeCode): - return workoutTypeCode - case .matchCancel: - return nil + case let .matchStart(matchStartRequest): return matchStartRequest + case let .isMatchedRandomPeer(isMatchedRandomPeersRequest): return isMatchedRandomPeersRequest + case .matchCancel: return nil } } diff --git a/iOS/Projects/Features/Record/Sources/Domain/Entities/Peer.swift b/iOS/Projects/Features/Record/Sources/Domain/Entities/Peer.swift new file mode 100644 index 00000000..233465c5 --- /dev/null +++ b/iOS/Projects/Features/Record/Sources/Domain/Entities/Peer.swift @@ -0,0 +1,14 @@ +// +// Peer.swift +// RecordFeature +// +// Created by MaraMincho on 12/5/23. +// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. +// + +import Foundation + +struct Peer { + let nickname: String + let imageURL: String +} diff --git a/iOS/Projects/Features/Record/Sources/Domain/Entities/SessionPeerType.swift b/iOS/Projects/Features/Record/Sources/Domain/Entities/SessionPeerType.swift index 8b4a1b67..3696ea40 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/Entities/SessionPeerType.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/Entities/SessionPeerType.swift @@ -17,5 +17,5 @@ struct SessionPeerType: Identifiable { let id: String /// 사용자의 프로필 이미지 주소 - let profileImageURL: URL + let profileImageURL: URL? } diff --git a/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSessionElement.swift b/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSessionElement.swift new file mode 100644 index 00000000..39b4f923 --- /dev/null +++ b/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSessionElement.swift @@ -0,0 +1,41 @@ +// +// WorkoutSessionElement.swift +// RecordFeature +// +// Created by MaraMincho on 12/5/23. +// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. +// + +import Foundation + +/// RandomMatching에서 WorkoutSession으로 화면전환 할 때 넘겨주는 데이터 입니다. +struct WorkoutSessionElement { + let startDate: Date + let peers: [Peer] + let roomID: String + + private let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:SS" + return formatter + }() + + /// 스트링으로 데이터를 받았을 때 서버에서 내려준 값이 + /// 현재 시간보다 과거거나, 서버에서 내려준 시간이 잘못되었을 경우 + /// 현재시간 + 4 초 후에 운동을 시작하는 것으로 세팅했습니다. + init(startDateString: String, peers: [Peer], roomID: String) { + var date = formatter.date(from: startDateString) ?? .now + 4 + if Date.now > date { + date = Date.now + 4 + } + startDate = date + self.peers = peers + self.roomID = roomID + } + + init(startDate: Date, peers: [Peer], roomID: String) { + self.startDate = startDate + self.peers = peers + self.roomID = roomID + } +} diff --git a/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSetting.swift b/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSetting.swift index d616fdf3..e2cfef1e 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSetting.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSetting.swift @@ -10,10 +10,14 @@ import Foundation // MARK: - WorkoutSetting +/// 어떤 운동을 할지와, 어떻게 운동을 할지 알려주는 객체 입니다. struct WorkoutSetting { let workoutType: WorkoutType + let workoutPeerType: PeerType + let isWorkoutAlone: Bool + init(workoutType: WorkoutType, workoutPeerType: PeerType, isWorkoutAlone: Bool = true) { self.workoutType = workoutType self.workoutPeerType = workoutPeerType diff --git a/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutType.swift b/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutType.swift index a609d69e..3281b00a 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutType.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutType.swift @@ -8,22 +8,29 @@ import Foundation +// MARK: - WorkoutType + /// 사용자가 어떤 운동을 하는지 알려주기 위해 사용하는 Entity입니다. /// 이 Entity를 통해 Card View를 보여줍니다. public struct WorkoutType: Hashable { /// Card에 나타나는 아이콘의 이미지 이름입니다. (SF Font 이미지 이름) - /// 다음과 같이 활용할 수 있습니다.UIimage(systemName: workoutType.workoutIcon) + /// 다음과 같이 활용할 수 있습니다. UIimage(systemName: workoutType.workoutIcon) let workoutIcon: String /// 운동이름을 나타냅니다. /// eg) 달리기 수영 사이클 let workoutTitle: String - // 어떤 운동을 하는지 숫자로 정의합니다. - // 중요: 서버측과 통신을 위해 Int로 만들었습니다. - // eg) 달리기는 1번, 사이클은... + /// 어떤 운동을 하는지 숫자로 정의합니다. + /// 중요: 서버측과 통신을 위해 Int로 만들었습니다. + /// eg) 달리기는 1번, 사이클은 2번, 수영은 3번 let typeCode: Int + /// 어떤 운동을 하는지 명시적으로 알게 해줍니다. enum Sports의 값으로 리턴 해줍니다. + var sports: Sports? { + return Sports(rawValue: typeCode) + } + init(workoutIcon: String, workoutIconDescription: String, typeCode: Int) { self.workoutIcon = workoutIcon workoutTitle = workoutIconDescription @@ -36,3 +43,11 @@ public struct WorkoutType: Hashable { typeCode = dto.typeCode } } + +// MARK: - Sports + +public enum Sports: Int { + case running = 1 + case cycling + case swimming +} diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/OneSecondsTimerUseCase.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/OneSecondsTimerUseCase.swift index 62dd75f3..cc2f675e 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/OneSecondsTimerUseCase.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/OneSecondsTimerUseCase.swift @@ -8,6 +8,7 @@ import Combine import Foundation +import Log // MARK: - OneSecondsTimerUseCaseRepresentable @@ -20,6 +21,7 @@ protocol OneSecondsTimerUseCaseRepresentable: TimerUseCaseRepresentable { final class OneSecondsTimerUseCase: TimerUseCase { override init(initDate: Date, timerPeriod: Double = 1) { super.init(initDate: initDate, timerPeriod: timerPeriod) + startTimer() } } @@ -27,7 +29,6 @@ final class OneSecondsTimerUseCase: TimerUseCase { extension OneSecondsTimerUseCase: OneSecondsTimerUseCaseRepresentable { func oneSecondsTimerPublisher() -> AnyPublisher { - startTimer() return intervalCurrentAndInitEverySecondsPublisher() .map { abs($0) } .eraseToAnyPublisher() diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/WorkoutPeerRandomMatchingRepositoryRepresentable.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/WorkoutPeerRandomMatchingRepositoryRepresentable.swift index 6dc3284e..178d8118 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/WorkoutPeerRandomMatchingRepositoryRepresentable.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/WorkoutPeerRandomMatchingRepositoryRepresentable.swift @@ -14,5 +14,5 @@ import Foundation protocol WorkoutPeerRandomMatchingRepositoryRepresentable { func matchStart(workoutTypeCode: Int) -> AnyPublisher, Never> func matchCancel() - func isMatchedRandomPeer(workoutTypeCode: Int) -> AnyPublisher, Never> + func isMatchedRandomPeer(isMatchedRandomPeersRequest: IsMatchedRandomPeersRequest) -> AnyPublisher, Never> } diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/TimerUseCase.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/TimerUseCase.swift index bdbf63e7..0ac6dfbc 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/TimerUseCase.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/TimerUseCase.swift @@ -59,7 +59,7 @@ private extension TimerUseCase { let timeInterval = initDate.timeIntervalSince(currentDate) let currentMillisecondsString = String(format: "%.2f", timeInterval) if Int(currentMillisecondsString.suffix(2)) == 0 { - if timeInterval.rounded(.toNearestOrAwayFromZero) >= timerPeriod { + if abs(timeInterval.rounded(.toNearestOrAwayFromZero)) >= timerPeriod { timeIntervalAtEachPeriodPublisher.send(Int(timeInterval.rounded(.toNearestOrAwayFromZero))) } startPeriodTimer() diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/UserInformationUseCase.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/UserInformationUseCase.swift new file mode 100644 index 00000000..54277d50 --- /dev/null +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/UserInformationUseCase.swift @@ -0,0 +1,75 @@ +// +// UserInformationUseCase.swift +// RecordFeature +// +// Created by MaraMincho on 12/6/23. +// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. +// + +import Foundation +import UserInformationManager + +// MARK: - UserInformationUseCaseRepresentable + +protocol UserInformationUseCaseRepresentable { + func userNickName() -> String + func userProfileImageData() -> Data? + func userProfileImageURL() -> URL? + func userProfileBirthDay() -> Date +} + +// MARK: - UserInformationUseCase + +/// 유저디폴트 매니저를 활용하여, 사용자의 개인정보를 가져옵니다. +struct UserInformationUseCase: UserInformationUseCaseRepresentable { + private let manager = UserInformationManager.shared + + private let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter + }() + + func userNickName() -> String { + guard + let nameData = manager.data(.userNickName), + let name = String(data: nameData, encoding: .utf8) + else { + return UserInformationDefaultsConstants.nickName + } + return name + } + + func userProfileImageData() -> Data? { + return manager.data(.userProfileImage) + } + + func userProfileImageURL() -> URL? { + guard + let imageURLStringData = manager.data(.userProfileImageURL), + let imageURLString = String(data: imageURLStringData, encoding: .utf8), + let imageURL = URL(string: imageURLString) + else { + return UserInformationDefaultsConstants.url + } + return imageURL + } + + func userProfileBirthDay() -> Date { + guard + let birthdayDateStringData = manager.data(.birthDayDate), + let birthdayDateString = String(data: birthdayDateStringData, encoding: .utf8), + let birthdayDate = formatter.date(from: birthdayDateString) + else { + return UserInformationDefaultsConstants.date + } + + return birthdayDate + } + + private enum UserInformationDefaultsConstants { + static let nickName = "김무디" + static let url = URL(string: "http://www.catster.com/wp-content/uploads/2017/08/Pixiebob-cat.jpg") + static let date = Date.now + } +} diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutEnvironmentSetupUseCase.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutEnvironmentSetupUseCase.swift index 755d03b3..be469289 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutEnvironmentSetupUseCase.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutEnvironmentSetupUseCase.swift @@ -13,7 +13,7 @@ import Foundation protocol WorkoutEnvironmentSetupUseCaseRepresentable { func workoutTypes() -> AnyPublisher, Never> - func paerTypes() -> AnyPublisher, Never> + func peerTypes() -> AnyPublisher, Never> } // MARK: - WorkoutEnvironmentSetupUseCase @@ -25,7 +25,7 @@ final class WorkoutEnvironmentSetupUseCase: WorkoutEnvironmentSetupUseCaseRepres self.repository = repository } - func paerTypes() -> AnyPublisher, Never> { + func peerTypes() -> AnyPublisher, Never> { return repository .peerType() .map { dto -> Result<[PeerType], Error> in diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutPeerRandomMatchingUseCase.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutPeerRandomMatchingUseCase.swift index 6ef40935..1b6fdc51 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutPeerRandomMatchingUseCase.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutPeerRandomMatchingUseCase.swift @@ -12,9 +12,9 @@ import Foundation // MARK: - WorkoutPeerRandomMatchingUseCaseRepresentable protocol WorkoutPeerRandomMatchingUseCaseRepresentable { - func matcheStart(workoutSetting: WorkoutSetting) -> AnyPublisher, Never> + func matchStart(workoutSetting: WorkoutSetting) -> AnyPublisher, Never> func matchCancel() - func isMatchedRandomPeer(workoutTypeCode: Int) -> AnyPublisher, Never> + func isMatchedRandomPeer(isMatchedRandomPeersRequest: IsMatchedRandomPeersRequest) -> AnyPublisher, Never> } // MARK: - WorkoutPeerRandomMatchingUseCase @@ -29,7 +29,7 @@ struct WorkoutPeerRandomMatchingUseCase { // MARK: WorkoutPeerRandomMatchingUseCaseRepresentable extension WorkoutPeerRandomMatchingUseCase: WorkoutPeerRandomMatchingUseCaseRepresentable { - func matcheStart(workoutSetting: WorkoutSetting) -> AnyPublisher, Never> { + func matchStart(workoutSetting: WorkoutSetting) -> AnyPublisher, Never> { return repository.matchStart(workoutTypeCode: workoutSetting.workoutType.typeCode) } @@ -37,8 +37,24 @@ extension WorkoutPeerRandomMatchingUseCase: WorkoutPeerRandomMatchingUseCaseRepr return repository.matchCancel() } - func isMatchedRandomPeer(workoutTypeCode: Int) -> AnyPublisher, Never> { - // TODO: DTO to Entity 변환 작업 필요 - return repository.isMatchedRandomPeer(workoutTypeCode: workoutTypeCode) + /// 만약 매칭이 잡혔으면 response의 값을 내려주고, + /// 매칭에 관한 request가 제대로 전달 되었지만 매칭이 잡히지 않았을 경우 nil을 내려준다. + func isMatchedRandomPeer( + isMatchedRandomPeersRequest: IsMatchedRandomPeersRequest + ) -> AnyPublisher, Never> { + return repository + .isMatchedRandomPeer(isMatchedRandomPeersRequest: isMatchedRandomPeersRequest) + .map { result -> Result in + switch result { + case let .failure(error): + return .failure(error) + case let .success(response): + if response?.matched == true { + return .success(response) + } + return .success(nil) + } + } + .eraseToAnyPublisher() } } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Delegate/WorkoutSettingCoordinatorFinishDelegate.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Delegate/WorkoutSettingCoordinatorFinishDelegate.swift index 9fbaa228..7ac1a87c 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Delegate/WorkoutSettingCoordinatorFinishDelegate.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Delegate/WorkoutSettingCoordinatorFinishDelegate.swift @@ -9,5 +9,5 @@ import Foundation protocol WorkoutSettingCoordinatorFinishDelegate: AnyObject { - func workoutSettingCoordinatorDidFinished(workoutSetting: WorkoutSetting) + func workoutSettingCoordinatorDidFinished(workoutSessionComponents: WorkoutSessionComponents) } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/RecordFeatureCoordinating.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/RecordFeatureCoordinating.swift index 98d4119c..a3996858 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/RecordFeatureCoordinating.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/RecordFeatureCoordinating.swift @@ -11,5 +11,5 @@ import Foundation protocol RecordFeatureCoordinating: Coordinating { func showSettingFlow() - func showWorkoutFlow(workoutSetting: WorkoutSetting) + func showWorkoutFlow(_ workoutSessionComponents: WorkoutSessionComponents) } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutEnvironmentSetUpCoordinating.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutEnvironmentSetUpCoordinating.swift index d1876d60..52e79666 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutEnvironmentSetUpCoordinating.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutEnvironmentSetUpCoordinating.swift @@ -13,6 +13,6 @@ protocol WorkoutEnvironmentSetUpCoordinating: Coordinating { func pushWorkoutSelectViewController() func pushWorkoutEnvironmentSetupViewController() func pushPeerRandomMatchingViewController(workoutSetting: WorkoutSetting) - func finish(workoutSetting: WorkoutSetting) + func finish(workoutSessionComponents: WorkoutSessionComponents) func popPeerRandomMatchingViewController() } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutSessionCoordinating.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutSessionCoordinating.swift index d379271e..b5e58847 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutSessionCoordinating.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutSessionCoordinating.swift @@ -15,7 +15,7 @@ protocol WorkoutSessionCoordinating: Coordinating { func pushWorkoutSummaryViewController(recordID: Int) /// 운동 화면으로 이동합니다. - func pushWorkoutSession(dependency: WorkoutSessionDependency) + func pushWorkoutSession() /// 운동전 카운트 다운 화면으로 이동합니다. func pushCountDownBeforeWorkout() diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/RecordFeatureCoordinator.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/RecordFeatureCoordinator.swift index cde82bcf..da00a931 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/RecordFeatureCoordinator.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/RecordFeatureCoordinator.swift @@ -67,8 +67,12 @@ public final class RecordFeatureCoordinator: RecordFeatureCoordinating { workoutSettingCoordinator.start() } - func showWorkoutFlow(workoutSetting _: WorkoutSetting) { - let coordinator = WorkoutSessionCoordinator(navigationController: navigationController, isMockEnvironment: true) + func showWorkoutFlow(_ workoutSessionComponents: WorkoutSessionComponents) { + let coordinator = WorkoutSessionCoordinator( + navigationController: navigationController, + isMockEnvironment: true, + workoutSessionComponents: workoutSessionComponents + ) childCoordinators.append(coordinator) coordinator.finishDelegate = self coordinator.start() @@ -89,7 +93,8 @@ extension RecordFeatureCoordinator: CoordinatorFinishDelegate { // MARK: WorkoutSettingCoordinatorFinishDelegate extension RecordFeatureCoordinator: WorkoutSettingCoordinatorFinishDelegate { - func workoutSettingCoordinatorDidFinished(workoutSetting: WorkoutSetting) { - showWorkoutFlow(workoutSetting: workoutSetting) + func workoutSettingCoordinatorDidFinished(workoutSessionComponents: WorkoutSessionComponents) { + navigationController.dismiss(animated: false) + showWorkoutFlow(workoutSessionComponents) } } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutEnvironmentSetUpCoordinator.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutEnvironmentSetUpCoordinator.swift index 3199f6b5..781d222e 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutEnvironmentSetUpCoordinator.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutEnvironmentSetUpCoordinator.swift @@ -37,8 +37,13 @@ final class WorkoutEnvironmentSetUpCoordinator: WorkoutEnvironmentSetUpCoordinat let repository = WorkoutEnvironmentSetupNetworkRepository(session: URLSession.shared) let useCase = WorkoutEnvironmentSetupUseCase(repository: repository) + let userInfoUseCase = UserInformationUseCase() - let viewModel = WorkoutEnvironmentSetupViewModel(useCase: useCase, coordinator: self) + let viewModel = WorkoutEnvironmentSetupViewModel( + workoutEnvironmentSetupUseCase: useCase, + userInformationUseCase: userInfoUseCase, + coordinator: self + ) let viewController = WorkoutEnvironmentSetupViewController(viewModel: viewModel) @@ -46,11 +51,18 @@ final class WorkoutEnvironmentSetUpCoordinator: WorkoutEnvironmentSetUpCoordinat } func pushPeerRandomMatchingViewController(workoutSetting: WorkoutSetting) { - let repository = WorkoutPeerRandomMatchingRepository(session: makeMockDataFromRandomMatching()) + let repository = WorkoutPeerRandomMatchingRepository(session: makeMockDataFromRandomMatching() + ) - let useCase = WorkoutPeerRandomMatchingUseCase(repository: repository) + let workoutPeerRandomMatchingUseCase = WorkoutPeerRandomMatchingUseCase(repository: repository) + let userInformationUseCase = UserInformationUseCase() - let viewModel = WorkoutPeerRandomMatchingViewModel(workoutSetting: workoutSetting, coordinating: self, useCase: useCase) + let viewModel = WorkoutPeerRandomMatchingViewModel( + workoutSetting: workoutSetting, + coordinating: self, + workoutPeerRandomMatchingUseCase: workoutPeerRandomMatchingUseCase, + userInformationUseCase: userInformationUseCase + ) let viewController = WorkoutPeerRandomMatchingViewController(viewModel: viewModel) @@ -63,8 +75,8 @@ final class WorkoutEnvironmentSetUpCoordinator: WorkoutEnvironmentSetUpCoordinat navigationController.dismiss(animated: true) } - func finish(workoutSetting: WorkoutSetting) { - settingDidFinishedDelegate?.workoutSettingCoordinatorDidFinished(workoutSetting: workoutSetting) + func finish(workoutSessionComponents: WorkoutSessionComponents) { + settingDidFinishedDelegate?.workoutSettingCoordinatorDidFinished(workoutSessionComponents: workoutSessionComponents) } } @@ -125,13 +137,13 @@ private extension WorkoutEnvironmentSetUpCoordinator { static let bundleIdentifier = "kr.codesquad.boostcamp8.RecordFeature" static let matchStart = "MatchesStart" - static let matchStartPath = "matches/start" + static let matchStartPath = "api/v1/matches/start" static let matchCancel = "matchesCancel" - static let matchCancelPath = "matches/cancle" + static let matchCancelPath = "api/v1/matches/cancel" static let matchesRandom = "MatchesRandom" - static let matchesRandomPath = "matches/random" + static let matchesRandomPath = "api/v1/matches/random" static let peerTypesFileNameOfType = "json" } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutSessionCoordinator.swift b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutSessionCoordinator.swift index 02d21a51..3c2781fb 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutSessionCoordinator.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/WorkoutSessionCoordinator.swift @@ -20,12 +20,69 @@ protocol WorkoutSessionDependency: WorkoutSessionUseCaseDependency, // MARK: - WorkoutSessionComponents +/// WorkoutSession을 진행하기 윈한 컴포넌트 요소입니다. struct WorkoutSessionComponents: WorkoutSessionDependency { + /// 참여하는 팀원들에 대한 배열입니다. let participants: [SessionPeerType] + + /// 매칭 시작 시간을 알 수 있습니다. let startDate: Date + + /// 서버로 부터 RoomID let roomID: String + + /// 서버로 부터 Public ID를 전달 받습니다. let id: String + + /// 어떤 운동인지 알 수 있습니다. + let workoutTypeCode: WorkoutType + + /// 실제 사용자(기기 사용자)의 닉네임 입니다. let nickname: String + + /// 실제 사용자(기기 사용자)의 프로필 이미지 URL 입니다. + let userProfileImage: URL? + + init( + participants: [SessionPeerType], + startDate: Date, + roomID: String, + id: String, + workoutTypeCode: WorkoutType, + nickname: String, + userProfileImage: URL? + ) { + self.participants = participants + self.startDate = startDate + self.roomID = roomID + self.id = id + self.workoutTypeCode = workoutTypeCode + self.nickname = nickname + self.userProfileImage = userProfileImage + } + + /// 서버에서 받아오는 String String으로 만들어진 Date값을 Formatter을 활용하여 Date로 바꾸었습니다. + init( + participants: [SessionPeerType], + startDate: String, + roomID: String, + id: String, + workoutTypeCode: WorkoutType, + nickname: String, + userProfileImage: URL? + ) { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-mm-dd hh:mm:ss" + let date = formatter.date(from: startDate) + + self.startDate = date ?? .now + 4 + self.participants = participants + self.roomID = roomID + self.id = id + self.workoutTypeCode = workoutTypeCode + self.nickname = nickname + self.userProfileImage = userProfileImage + } } // MARK: - WorkoutSessionCoordinator @@ -36,17 +93,21 @@ final class WorkoutSessionCoordinator: WorkoutSessionCoordinating { weak var finishDelegate: CoordinatorFinishDelegate? var flow: CoordinatorFlow = .workout private let isMockEnvironment: Bool + private let workoutSessionComponents: WorkoutSessionComponents - init(navigationController: UINavigationController, isMockEnvironment: Bool) { + init(navigationController: UINavigationController, isMockEnvironment: Bool, workoutSessionComponents: WorkoutSessionComponents) { self.navigationController = navigationController self.isMockEnvironment = isMockEnvironment + self.workoutSessionComponents = workoutSessionComponents } func start() { pushCountDownBeforeWorkout() } - func pushWorkoutSession(dependency: WorkoutSessionDependency) { + func pushWorkoutSession() { + let oneSecondsTimerUseCase = OneSecondsTimerUseCase(initDate: .now) + guard let jsonPath = Bundle(for: Self.self).path(forResource: "WorkoutSession", ofType: "json"), let jsonData = try? Data(contentsOf: .init(filePath: jsonPath)) else { @@ -57,24 +118,25 @@ final class WorkoutSessionCoordinator: WorkoutSessionCoordinating { let healthRepository = HealthRepository() // TODO: 같이하기, 혼자하기 모드에 따라 session 주입을 다르게 해야합니다. - let socketRepository = WorkoutSocketRepository(session: MockWebSocketSession(), dependency: dependency) + let socketRepository = WorkoutSocketRepository(session: MockWebSocketSession(), dependency: workoutSessionComponents) let sessionUseCase = WorkoutSessionUseCase( healthRepository: healthRepository, socketRepository: socketRepository, - dependency: dependency + dependency: workoutSessionComponents ) let sessionViewModel = WorkoutSessionViewModel(useCase: sessionUseCase) - let sessionViewController = WorkoutSessionViewController(viewModel: sessionViewModel, dependency: dependency) + let sessionViewController = WorkoutSessionViewController(viewModel: sessionViewModel, dependency: workoutSessionComponents) let session: URLSessionProtocol = isMockEnvironment ? MockURLSession(mockData: jsonData) : URLSession.shared let repository = WorkoutRecordRepository(session: session) let useCase = WorkoutRecordUseCase(repository: repository) let viewModel = WorkoutSessionContainerViewModel( workoutRecordUseCase: useCase, + oneSecondsTimerUseCase: oneSecondsTimerUseCase, coordinating: self, - dependency: dependency + dependency: workoutSessionComponents ) let viewController = WorkoutSessionContainerViewController(viewModel: viewModel, healthDataProtocol: sessionViewController) @@ -99,7 +161,7 @@ final class WorkoutSessionCoordinator: WorkoutSessionCoordinating { func pushCountDownBeforeWorkout() { // TODO: CountDown 관련 ViewController 생성 - let useCase = CountDownBeforeWorkoutStartTimerUseCase(initDate: .now + 2) + let useCase = CountDownBeforeWorkoutStartTimerUseCase(initDate: workoutSessionComponents.startDate) let viewModel = CountDownBeforeWorkoutViewModel(coordinator: self, useCase: useCase) diff --git a/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewController/CountDownBeforeWorkoutViewController.swift b/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewController/CountDownBeforeWorkoutViewController.swift index 799148de..ecd05587 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewController/CountDownBeforeWorkoutViewController.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewController/CountDownBeforeWorkoutViewController.swift @@ -125,7 +125,7 @@ private extension CountDownBeforeWorkoutViewController { } func makeLabelAnimation(labelText: String) { - Log.make().debug("viewController makeLabelAnimation: \(labelText)") + Log.make().debug("\(labelText) 뒤에 운동을 시작합니다") countDownLabel.text = labelText countDownLabel.transform = CGAffineTransform(scaleX: 1, y: 1) view.layoutIfNeeded() diff --git a/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewModel/CountDownBeforeWorkoutViewModel.swift b/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewModel/CountDownBeforeWorkoutViewModel.swift index 6baea640..c7cc10a7 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewModel/CountDownBeforeWorkoutViewModel.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/CountDownBeforeWorkoutScene/ViewModel/CountDownBeforeWorkoutViewModel.swift @@ -38,13 +38,12 @@ final class CountDownBeforeWorkoutViewModel { // MARK: - Properties weak var coordinator: WorkoutSessionCoordinating? - var useCase: CountDownBeforeWorkoutStartTimerUseCaseRepresentable - // TODO: 차후 생성 시점에서 시작 시간을 넘길 예정 + var timerUseCase: CountDownBeforeWorkoutStartTimerUseCaseRepresentable private var subscriptions: Set = [] private var beforeWorkoutTimerSubject: CurrentValueSubject = .init("") init(coordinator: WorkoutSessionCoordinating, useCase: CountDownBeforeWorkoutStartTimerUseCaseRepresentable) { self.coordinator = coordinator - self.useCase = useCase + timerUseCase = useCase } } @@ -56,27 +55,19 @@ extension CountDownBeforeWorkoutViewModel: CountDownBeforeWorkoutViewModelRepres input .viewDidAppearPublisher - .sink { [weak self] _ in - self?.useCase.startTimer() + .sink { [timerUseCase] _ in + timerUseCase.startTimer() } .store(in: &subscriptions) input .didFinishTimerSubscription .sink { [weak self] _ in - self?.coordinator?.pushWorkoutSession( - dependency: WorkoutSessionComponents( - participants: [.init(nickname: "S043_홍승현", id: "MyID", profileImageURL: .init(filePath: ""))], - startDate: .now, - roomID: "HHH", - id: "MyID", - nickname: "S043_홍승현" - ) - ) + self?.coordinator?.pushWorkoutSession() } .store(in: &subscriptions) - let timerMessagePublisher = useCase + let timerMessagePublisher = timerUseCase .beforeWorkoutTimerTextPublisher() .map { message -> CountDownBeforeWorkoutState in return .updateMessage(message: message) diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewController/WorkoutEnvironmentSetupViewController.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewController/WorkoutEnvironmentSetupViewController.swift index 802bc5af..72b92152 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewController/WorkoutEnvironmentSetupViewController.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewController/WorkoutEnvironmentSetupViewController.swift @@ -142,7 +142,7 @@ private extension WorkoutEnvironmentSetupViewController { // TODO: failure에 알맞는 로직 세우기 case .error, .idle: break - case let .workoutTpyes(workoutTypes): updateWorkout(types: workoutTypes) + case let .workoutTypes(workoutTypes): updateWorkout(types: workoutTypes) case let .workoutPeerTypes(peer): updateWorkoutPeer(types: peer) case let .didSelectWorkoutType(bool): workoutSelectViewController.nextButtonEnable(bool) case let .didSelectWorkoutPeerType(bool): workoutPeerSelectViewController.startButtonEnable(bool) diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewModel/WorkoutEnvironmentSetupViewModel.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewModel/WorkoutEnvironmentSetupViewModel.swift index 460b53c3..1d2a3dd0 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewModel/WorkoutEnvironmentSetupViewModel.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutEnvironmentScene/ViewModel/WorkoutEnvironmentSetupViewModel.swift @@ -26,7 +26,7 @@ typealias WorkoutEnvironmentSetupViewModelOutput = AnyPublisher() - private var useCase: WorkoutEnvironmentSetupUseCaseRepresentable + private let workoutEnvironmentSetupUseCase: WorkoutEnvironmentSetupUseCaseRepresentable + private let userInformationUseCase: UserInformationUseCaseRepresentable private weak var coordinator: WorkoutEnvironmentSetUpCoordinating? @@ -60,11 +61,13 @@ final class WorkoutEnvironmentSetupViewModel { var workoutTypes: [WorkoutType] = [] init( - useCase: WorkoutEnvironmentSetupUseCaseRepresentable, + workoutEnvironmentSetupUseCase: WorkoutEnvironmentSetupUseCaseRepresentable, + userInformationUseCase: UserInformationUseCaseRepresentable, coordinator: WorkoutEnvironmentSetUpCoordinator? ) { - self.useCase = useCase + self.workoutEnvironmentSetupUseCase = workoutEnvironmentSetupUseCase self.coordinator = coordinator + self.userInformationUseCase = userInformationUseCase } } @@ -76,29 +79,21 @@ extension WorkoutEnvironmentSetupViewModel: WorkoutEnvironmentSetupViewModelRepr let workoutTypes: WorkoutEnvironmentSetupViewModelOutput = input .requestWorkoutTypes - .flatMap { [weak self] _ -> AnyPublisher, Never> in - guard let self else { - return Just(Result.failure(ViewModelError.viewModelDidDeinit)).eraseToAnyPublisher() - } - return useCase.workoutTypes() - } + .flatMap(workoutEnvironmentSetupUseCase.workoutTypes) .map { results -> WorkoutEnvironmentState in switch results { - case let .success(workOuttypes): - let uniquePeerTypes = Array(Set(workOuttypes)) - return .workoutTpyes(uniquePeerTypes) + case let .success(workoutTypes): + let uniquePeerTypes = Array(Set(workoutTypes)) + return .workoutTypes(uniquePeerTypes) case .failure: - return .error(.unkownError) + return .error(.unknownError) } }.eraseToAnyPublisher() let workoutPeerType: WorkoutEnvironmentSetupViewModelOutput = input .requestWorkoutPeerTypes - .flatMap { [weak self] _ -> AnyPublisher, Never> in - guard let self else { - return Just(Result.failure(ViewModelError.viewModelDidDeinit)).eraseToAnyPublisher() - } - return useCase.paerTypes() + .flatMap { [workoutEnvironmentSetupUseCase] _ -> AnyPublisher, Never> in + return workoutEnvironmentSetupUseCase.peerTypes() } .map { results -> WorkoutEnvironmentState in switch results { @@ -106,7 +101,7 @@ extension WorkoutEnvironmentSetupViewModel: WorkoutEnvironmentSetupViewModelRepr let uniquePeerTypes = Array(Set(peerTypes)) return .workoutPeerTypes(uniquePeerTypes) case .failure: - return .error(.unkownError) + return .error(.unknownError) } }.eraseToAnyPublisher() @@ -114,7 +109,7 @@ extension WorkoutEnvironmentSetupViewModel: WorkoutEnvironmentSetupViewModelRepr .selectPeerType .map { [weak self] peerType -> WorkoutEnvironmentState in guard let self else { - return .error(.unkownError) + return .error(.unknownError) } if let peerType { self.didSelectWorkoutPeerType = peerType @@ -127,7 +122,7 @@ extension WorkoutEnvironmentSetupViewModel: WorkoutEnvironmentSetupViewModelRepr .selectWorkoutType .map { [weak self] workoutType -> WorkoutEnvironmentState in guard let self else { - return .error(.unkownError) + return .error(.unknownError) } if let workoutType { self.didSelectWorkoutType = workoutType @@ -156,13 +151,28 @@ extension WorkoutEnvironmentSetupViewModel: WorkoutEnvironmentSetupViewModelRepr return } - let workoutSettiong = WorkoutSetting(workoutType: didSelectWorkoutType, workoutPeerType: didSelectWorkoutPeerType) + let workoutSetting = WorkoutSetting(workoutType: didSelectWorkoutType, workoutPeerType: didSelectWorkoutPeerType) switch mode { case .solo: - coordinator?.finish(workoutSetting: workoutSettiong) + let sessionPeerTypeOfMe = SessionPeerType( + nickname: userInformationUseCase.userNickName(), + id: "", + profileImageURL: userInformationUseCase.userProfileImageURL() + ) + coordinator?.finish( + workoutSessionComponents: .init( + participants: [sessionPeerTypeOfMe], + startDate: .now + 3, + roomID: "", + id: "", + workoutTypeCode: workoutSetting.workoutType, + nickname: userInformationUseCase.userNickName(), + userProfileImage: userInformationUseCase.userProfileImageURL() + ) + ) case .random: - coordinator?.pushPeerRandomMatchingViewController(workoutSetting: workoutSettiong) + coordinator?.pushPeerRandomMatchingViewController(workoutSetting: workoutSetting) } } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutPeerMatchingScene/ViewModel/WorkoutPeerRandomMatchingViewModel.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutPeerMatchingScene/ViewModel/WorkoutPeerRandomMatchingViewModel.swift index 810620db..2eb31b99 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutPeerMatchingScene/ViewModel/WorkoutPeerRandomMatchingViewModel.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutPeerMatchingScene/ViewModel/WorkoutPeerRandomMatchingViewModel.swift @@ -8,6 +8,7 @@ import Combine import Foundation +import Log // MARK: - WorkoutPeerRandomMatchingViewModelInput @@ -35,20 +36,24 @@ final class WorkoutPeerRandomMatchingViewModel { // MARK: - Properties private weak var coordinating: WorkoutEnvironmentSetUpCoordinating? - private var useCase: WorkoutPeerRandomMatchingUseCaseRepresentable + private let workoutPeerRandomMatchingUseCase: WorkoutPeerRandomMatchingUseCaseRepresentable + private let userInformationUseCase: UserInformationUseCaseRepresentable private let workoutSetting: WorkoutSetting private var timerInitDate: Date? init( workoutSetting: WorkoutSetting, coordinating: WorkoutEnvironmentSetUpCoordinating, - useCase: WorkoutPeerRandomMatchingUseCaseRepresentable + workoutPeerRandomMatchingUseCase: WorkoutPeerRandomMatchingUseCaseRepresentable, + userInformationUseCase: UserInformationUseCaseRepresentable ) { self.coordinating = coordinating - self.useCase = useCase + self.workoutPeerRandomMatchingUseCase = workoutPeerRandomMatchingUseCase + self.userInformationUseCase = userInformationUseCase self.workoutSetting = workoutSetting } + private var didMatchStartedDate = Date.now private var subscriptions: Set = [] } @@ -73,14 +78,17 @@ extension WorkoutPeerRandomMatchingViewModel: WorkoutPeerRandomMatchingViewModel } private func bindUseCase() { - useCase.matcheStart(workoutSetting: workoutSetting) + workoutPeerRandomMatchingUseCase + .matchStart(workoutSetting: workoutSetting) .receive(on: RunLoop.main) .sink { [weak self] results in switch results { case .failure: + Log.make().error("1단계 매칭 스타트 요청을 실패했습니다.") self?.cancelPeerRandomMatching() case .success: - self?.startIsMatchedRandomPeer(every: Constants.pollingPeroid) + Log.make().error("1단계 매칭 스타트 요청을 시작했습니다.") + self?.startIsMatchedRandomPeer(every: Constants.pollingPeriod) self?.cancelPeerRandomMatching(after: Constants.maximumCouldWaitTime) } } @@ -98,38 +106,80 @@ extension WorkoutPeerRandomMatchingViewModel: WorkoutPeerRandomMatchingViewModel } private func startIsMatchedRandomPeer(every time: Double) { + didMatchStartedDate = .now Timer.publish(every: time, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in - self?.sendIsMatchedRandomPeer() + guard let self else { + return + } + let waitingTime = Date.now.timeIntervalSince(didMatchStartedDate) + let request = IsMatchedRandomPeersRequest(workoutID: workoutSetting.workoutType.typeCode, waitingTime: Int(waitingTime)) + requestIsMatchedRandomPeers(request: request) } .store(in: &subscriptions) } - private func sendIsMatchedRandomPeer() { - useCase - .isMatchedRandomPeer(workoutTypeCode: workoutSetting.workoutType.typeCode) + /// matche를 한다고 계속 요청을 보냅니다. + /// UseCase쪽에서 매치가 되었다고 한다면, + /// 매치 성사 되었을 떄: pushWorkoutSession 함수로 coordinator를 통해서 다음 화면으로 넘어갑니다. + /// 매치 성사가 안 되었을 때: 2초마다 계속해서 요청을 보냅니다. + func requestIsMatchedRandomPeers(request: IsMatchedRandomPeersRequest) { + workoutPeerRandomMatchingUseCase + .isMatchedRandomPeer(isMatchedRandomPeersRequest: request) .receive(on: RunLoop.main) .sink { [weak self] result in switch result { - case let .success(dto): - if dto == nil { - break + case let .success(response): + if response == nil { + return } - case .failure: + self?.pushWorkoutSession(response: response) + case let .failure(error): + Log.make().error("\(error)") self?.coordinating?.popPeerRandomMatchingViewController() } } .store(in: &subscriptions) } + func pushWorkoutSession(response: IsMatchedRandomPeersResponse?) { + subscriptions.removeAll() + guard + let response, + let peersResponse = response.peers, + let roomID = response.roomID, + let startDate = response.liveWorkoutStartTime, + let id = response.publicID + else { + return + } + let peers = peersResponse.map { SessionPeerType(nickname: $0.nickname, id: $0.publicID, profileImageURL: URL(string: $0.profileImage)) } + let sessionPeerTypeOfMe = SessionPeerType( + nickname: userInformationUseCase.userNickName(), + id: id, + profileImageURL: userInformationUseCase.userProfileImageURL() + ) + + let workoutSessionComponents = WorkoutSessionComponents( + participants: [sessionPeerTypeOfMe] + peers, + startDate: startDate, + roomID: roomID, + id: id, + workoutTypeCode: workoutSetting.workoutType, + nickname: sessionPeerTypeOfMe.nickname, + userProfileImage: sessionPeerTypeOfMe.profileImageURL + ) + coordinating?.finish(workoutSessionComponents: workoutSessionComponents) + } + private func cancelPeerRandomMatching() { - useCase.matchCancel() + workoutPeerRandomMatchingUseCase.matchCancel() coordinating?.popPeerRandomMatchingViewController() } private enum Constants { - static let pollingPeroid: Double = 2 - static let maximumCouldWaitTime: Double = 150 + static let pollingPeriod: Double = 2 + static let maximumCouldWaitTime: Double = 10 } } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/SessionScene/SessionParticipantCell.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/SessionScene/SessionParticipantCell.swift index d4dc5537..c838c324 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/SessionScene/SessionParticipantCell.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/SessionScene/SessionParticipantCell.swift @@ -185,8 +185,16 @@ final class SessionParticipantCell: UICollectionViewCell { guard isInitiallyConfigured == false else { return } isInitiallyConfigured = true distanceLabel.text = "0" - profileImageView.image = try? UIImage(data: Data(contentsOf: model.profileImageURL)) ?? .init(systemName: "person") nicknameLabel.text = model.nickname + guard let url = model.profileImageURL else { + return + } + DispatchQueue.global().async { [weak self] in + let image = try? UIImage(data: Data(contentsOf: url)) + DispatchQueue.main.async { [weak self] in + self?.profileImageView.image = image ?? .init(systemName: "person") + } + } } func configure(with model: WorkoutHealthRealTimeModel?) { diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewController.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewController.swift index 297a94b9..29048997 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewController.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewController.swift @@ -163,14 +163,9 @@ final class WorkoutSessionContainerViewController: UIViewController { .receive(on: RunLoop.main) .sink { [weak self] state in switch state { - case .idle: - break - case let .updateTime(elapsedTime): - let minutes = Int(-elapsedTime) / 60 % 60 - let seconds = Int(-elapsedTime) % 60 - self?.recordTimerLabel.text = String(format: "%02d분 %02d초", minutes, seconds) - case let .alert(error): - self?.showAlert(with: error) + case .idle: break + case let .updateTime(elapsedTime): self?.updateRecordTimerLabel(elapsedTime: elapsedTime) + case let .alert(error): self?.showAlert(with: error) } } .store(in: &subscriptions) @@ -178,6 +173,12 @@ final class WorkoutSessionContainerViewController: UIViewController { // MARK: - Custom Methods + private func updateRecordTimerLabel(elapsedTime: Int) { + let minutes = elapsedTime / 60 % 60 + let seconds = elapsedTime % 60 + recordTimerLabel.text = String(format: "%02d분 %02d초", minutes, seconds) + } + /// 에러 알림 문구를 보여줍니다. private func showAlert(with error: Error) { let alertController = UIAlertController(title: "알림", message: error.localizedDescription, preferredStyle: .alert) diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewModel.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewModel.swift index 06dd1718..32642881 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewModel.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/WorkoutSessionContainerViewModel.swift @@ -30,7 +30,7 @@ public typealias WorkoutSessionContainerViewModelOutput = AnyPublisher = [] + private let oneSecondsTimerUseCase: OneSecondsTimerUseCaseRepresentable + private let workoutRecordUseCase: WorkoutRecordUseCaseRepresentable private weak var coordinating: WorkoutSessionCoordinating? @@ -55,12 +57,14 @@ final class WorkoutSessionContainerViewModel { init( workoutRecordUseCase: WorkoutRecordUseCaseRepresentable, + oneSecondsTimerUseCase: OneSecondsTimerUseCaseRepresentable, coordinating: WorkoutSessionCoordinating, dependency: WorkoutSessionViewModelDependency ) { self.workoutRecordUseCase = workoutRecordUseCase self.coordinating = coordinating self.dependency = dependency + self.oneSecondsTimerUseCase = oneSecondsTimerUseCase } } @@ -106,9 +110,8 @@ extension WorkoutSessionContainerViewModel: WorkoutSessionContainerViewModelRepr .catch { return Just(.alert($0)) } .eraseToAnyPublisher() - let workoutTimerPublisher = Timer.publish(every: 1, on: .main, in: .common) - .autoconnect() - .map(dependency.startDate.timeIntervalSince(_:)) + let workoutTimerPublisher = oneSecondsTimerUseCase + .oneSecondsTimerPublisher() .map { WorkoutSessionContainerState.updateTime($0) } return Just(WorkoutSessionContainerState.idle).merge(with: recordErrorPublisher, workoutTimerPublisher).eraseToAnyPublisher() diff --git a/iOS/Projects/Shared/CommonNetworkingKeyManager/Sources/TNKeychainInterceptor.swift b/iOS/Projects/Shared/CommonNetworkingKeyManager/Sources/TNKeychainInterceptor.swift index 7ed978ec..09d47fd2 100644 --- a/iOS/Projects/Shared/CommonNetworkingKeyManager/Sources/TNKeychainInterceptor.swift +++ b/iOS/Projects/Shared/CommonNetworkingKeyManager/Sources/TNKeychainInterceptor.swift @@ -12,6 +12,8 @@ import Trinet // MARK: - TNKeychainInterceptor +/// 앱에서 사용되는 공통의 인터셉터 입니다. +/// 인터셉터를 통해서 자동으로 Header에 토큰을 넣어주고, 토큰 만료시 리프레시 로직을 던저줍니다. public final class TNKeychainInterceptor { private let decoder = JSONDecoder() public static let shared = TNKeychainInterceptor() diff --git a/iOS/Projects/Shared/UserInformationManager/Sources/UserInformationManager.swift b/iOS/Projects/Shared/UserInformationManager/Sources/UserInformationManager.swift index 0871b41a..f5ea2bea 100644 --- a/iOS/Projects/Shared/UserInformationManager/Sources/UserInformationManager.swift +++ b/iOS/Projects/Shared/UserInformationManager/Sources/UserInformationManager.swift @@ -24,15 +24,8 @@ public final class UserInformationManager { return formatter }() - /// Memory Cache에 있는 데이터를 리턴합니다. - /// - /// 중요: birthDayDate의 경우 yyyy-MM-dd의 포멧을 사용하는 String의 Data를 리턴합니다. - func data(_ key: UserInformation) -> Data? { - return defaults.data(forKey: key.rawValue) - } - public enum UserInformation: String, CaseIterable { - case userName = "UserNickName" + case userNickName = "UserNickName" case userProfileImage = "UserProfileImage" case birthDayDate = "BirthDayDate" case userProfileImageURL = "UserImageURL" @@ -40,13 +33,20 @@ public final class UserInformationManager { } public extension UserInformationManager { + /// UserDefaults에 있는 데이터를 리턴합니다. + /// + /// 중요: birthDayDate의 경우 yyyy-MM-dd의 포멧을 사용하는 String의 Data를 리턴합니다. + func data(_ key: UserInformation) -> Data? { + return defaults.data(forKey: key.rawValue) + } + func setUserName(_ name: String) { let nameData = Data(name.utf8) - defaults.setValue(nameData, forKey: UserInformation.userName.rawValue) + defaults.setValue(nameData, forKey: UserInformation.userNickName.rawValue) } func setUserProfileImageData(_ imageData: Data) { - defaults.setValue(imageData, forKey: UserInformation.userName.rawValue) + defaults.setValue(imageData, forKey: UserInformation.userNickName.rawValue) } func setBirthDayDate(_ date: Date) { @@ -61,7 +61,7 @@ private extension UserInformationManager { /// 만약 userDefaults에 값이 존재한다면 fakeData를 설정합니다. func setDefaultsData() { guard - data(.userName) == nil, + data(.userNickName) == nil, data(.birthDayDate) == nil, data(.userProfileImage) == nil, data(.userProfileImageURL) == nil @@ -76,13 +76,12 @@ private extension UserInformationManager { let dateData = Data(dateString.utf8) defaults.setValue(dateData, forKey: UserInformation.birthDayDate.rawValue) - let name = Data("김무드".utf8) - defaults.setValue(name, forKey: UserInformation.userName.rawValue) + let name = Data("김무디".utf8) + defaults.setValue(name, forKey: UserInformation.userNickName.rawValue) - guard let imageURL = URL(string: "https://www.catster.com/wp-content/uploads/2017/08/Pixiebob-cat.jpg") else { - return - } - defaults.setValue(imageURL, forKey: UserInformation.userProfileImageURL.rawValue) + let imageURLString = "https://www.catster.com/wp-content/uploads/2017/08/Pixiebob-cat.jpg" + let imageURLData = Data(imageURLString.utf8) + defaults.setValue(imageURLData, forKey: UserInformation.userProfileImageURL.rawValue) guard let path = Bundle(for: Self.self).path(forResource: DefaultsKey.imageKey, ofType: DefaultsKey.imageType),