diff --git a/iOS/Layover/Layover/Scenes/Map/MapRouter.swift b/iOS/Layover/Layover/Scenes/Map/MapRouter.swift index 861c5a3..52538ac 100644 --- a/iOS/Layover/Layover/Scenes/Map/MapRouter.swift +++ b/iOS/Layover/Layover/Scenes/Map/MapRouter.swift @@ -50,6 +50,6 @@ final class MapRouter: MapRoutingLogic, MapDataPassing { private func passDataToPlayback(source: MapDataStore, destination: inout PlaybackDataStore) { destination.posts = source.posts destination.index = source.postPlayStartIndex - destination.parentView = .other + destination.parentView = .map } } diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackInteractor.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackInteractor.swift index b662bf7..6b6d518 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackInteractor.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackInteractor.swift @@ -29,6 +29,8 @@ protocol PlaybackBusinessLogic { func resumeVideo() func moveToProfile(with request: PlaybackModels.MoveToRelativeView.Request) func moveToTagPlay(with request: PlaybackModels.MoveToRelativeView.Request) + @discardableResult + func fetchPosts() -> Task<Bool, Never> } protocol PlaybackDataStore: AnyObject { @@ -67,6 +69,10 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { var selectedTag: String? + private var isFetchReqeust: Bool = false + + private var currentPage: Int = 1 + // MARK: - UseCase Load Video List func displayVideoList() -> Task<Bool, Never> { @@ -74,7 +80,7 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { guard let parentView: Models.ParentView, var posts: [Post], let worker: PlaybackWorkerProtocol else { return false } - if parentView == .other { + if parentView == .map { posts = worker.makeInfiniteScroll(posts: posts) self.posts = posts } @@ -91,24 +97,17 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { guard let parentView, let index else { return } - switch parentView { - case .home, .myProfile: - presenter?.presentMoveInitialPlaybackCell(with: Models.SetInitialPlaybackCell.Response(indexPathRow: index)) - case .other: - presenter?.presentSetCellIfInfinite(with: Models.SetInitialPlaybackCell.Response(indexPathRow: index + 1)) - } + let willMoveIndex: Int + willMoveIndex = parentView == .map ? index + 1 : index + presenter?.presentMoveInitialPlaybackCell(with: Models.SetInitialPlaybackCell.Response(indexPathRow: willMoveIndex)) } func setInitialPlaybackCell() { guard let parentView, let index else { return } - let response: Models.SetInitialPlaybackCell.Response - switch parentView { - case .home, .myProfile: - response = Models.SetInitialPlaybackCell.Response(indexPathRow: index) - case .other: - response = Models.SetInitialPlaybackCell.Response(indexPathRow: index + 1) - } + let willMoveIndex: Int + willMoveIndex = parentView == .map ? index + 1 : index + let response: Models.SetInitialPlaybackCell.Response = Models.SetInitialPlaybackCell.Response(indexPathRow: willMoveIndex) presenter?.presentSetInitialPlaybackCell(with: response) } @@ -129,8 +128,8 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { isTeleport = false return } - // Home이 아닌 다른 뷰에서 왔을 경우(로드한 목록 무한 반복) - if parentView == .other { + // map에서 왔을 경우(로드한 목록 무한 반복) + if parentView == .map { if request.indexPathRow == (posts.count - 1) { response = Models.DisplayPlaybackVideo.Response(indexPathRow: 1, previousCell: previousCell, currentCell: nil) } else if request.indexPathRow == 0 { @@ -146,7 +145,7 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { presenter?.presentTeleportCell(with: response) return } - // Home이면 다음 셀로 이동(추가적인 비디오 로드) + // map이 아니면 다음 셀로 이동(추가적인 비디오 로드) isTeleport = false response = Models.DisplayPlaybackVideo.Response(previousCell: previousCell, currentCell: request.currentCell) previousCell = request.currentCell @@ -205,13 +204,9 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { func configurePlaybackCell() { guard let posts, let parentView else { return } - let response: Models.ConfigurePlaybackCell.Response - switch parentView { - case .home, .myProfile: - response = Models.ConfigurePlaybackCell.Response(teleportIndex: nil) - case .other: - response = Models.ConfigurePlaybackCell.Response(teleportIndex: posts.count + 1) - } + let willMoveTeleportIndex: Int? + willMoveTeleportIndex = parentView == .map ? posts.count + 1 : nil + let response: Models.ConfigurePlaybackCell.Response = Models.ConfigurePlaybackCell.Response(teleportIndex: willMoveTeleportIndex) presenter?.presentConfigureCell(with: response) } @@ -289,4 +284,45 @@ final class PlaybackInteractor: PlaybackBusinessLogic, PlaybackDataStore { self.selectedTag = selectedTag presenter?.presentTagPlay() } + + func fetchPosts() -> Task<Bool, Never> { + Task { + if !isFetchReqeust { + isFetchReqeust = true + var page: Int = 0 + if parentView != .home { + guard let posts else { return false } + page = posts.count / 15 + 1 + if page == currentPage { + return false + } + } + currentPage = page + var newPosts: [Post]? + switch parentView { + case .home: + newPosts = await worker?.fetchHomePosts() + case .map: + return false + case .myProfile, .otherProfile: + newPosts = await worker?.fetchProfilePosts(profileID: memberID, page: page) + case .tag: + guard let selectedTag else { return false } + newPosts = await worker?.fetchTagPosts(selectedTag: selectedTag, page: page) + default: + return false + } + guard let newPosts else { return false } + self.posts?.append(contentsOf: newPosts) + let videos: [Models.PlaybackVideo] = await transPostToVideo(newPosts) + let response: Models.LoadPlaybackVideoList.Response = Models.LoadPlaybackVideoList.Response(videos: videos) + await MainActor.run { + presenter?.presentLoadFetchVideos(with: response) + isFetchReqeust = false + } + return true + } + return false + } + } } diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift index 5f7686f..f53b77d 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift @@ -18,7 +18,9 @@ enum PlaybackModels { enum ParentView { case home case myProfile - case other + case otherProfile + case tag + case map } struct DisplayedPost: Hashable { diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift index 8ea8303..75ff4ba 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift @@ -10,6 +10,7 @@ import Foundation protocol PlaybackPresentationLogic { func presentVideoList(with response: PlaybackModels.LoadPlaybackVideoList.Response) + func presentLoadFetchVideos(with response: PlaybackModels.LoadPlaybackVideoList.Response) func presentSetCellIfInfinite(with response: PlaybackModels.SetInitialPlaybackCell.Response) func presentMoveCellNext(with response: PlaybackModels.DisplayPlaybackVideo.Response) func presentSetInitialPlaybackCell(with response: PlaybackModels.SetInitialPlaybackCell.Response) @@ -42,6 +43,11 @@ final class PlaybackPresenter: PlaybackPresentationLogic { viewController?.displayVideoList(viewModel: viewModel) } + func presentLoadFetchVideos(with response: PlaybackModels.LoadPlaybackVideoList.Response) { + let viewModel: Models.LoadPlaybackVideoList.ViewModel = Models.LoadPlaybackVideoList.ViewModel(videos: response.videos) + viewController?.loadFetchVideos(viewModel: viewModel) + } + func presentSetCellIfInfinite(with response: PlaybackModels.SetInitialPlaybackCell.Response) { viewController?.displayMoveCellIfinfinite(viewModel: Models.SetInitialPlaybackCell.ViewModel(indexPathRow: response.indexPathRow)) } diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift index 6206328..6416113 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift @@ -18,6 +18,7 @@ protocol PlaybackViewControllerDelegate: AnyObject { protocol PlaybackDisplayLogic: AnyObject { func displayVideoList(viewModel: PlaybackModels.LoadPlaybackVideoList.ViewModel) + func loadFetchVideos(viewModel: PlaybackModels.LoadPlaybackVideoList.ViewModel) func displayMoveCellIfinfinite(viewModel: PlaybackModels.SetInitialPlaybackCell.ViewModel) func stopPrevPlayerAndPlayCurPlayer(viewModel: PlaybackModels.DisplayPlaybackVideo.ViewModel) func setInitialPlaybackCell(viewModel: PlaybackModels.SetInitialPlaybackCell.ViewModel) @@ -188,6 +189,12 @@ extension PlaybackViewController: PlaybackDisplayLogic { dataSource?.apply(snapshot, animatingDifferences: false) } + func loadFetchVideos(viewModel: PlaybackModels.LoadPlaybackVideoList.ViewModel) { + guard var currentSnapshot = dataSource?.snapshot() else { return } + currentSnapshot.appendItems(viewModel.videos) + dataSource?.apply(currentSnapshot, animatingDifferences: true) + } + func displayMoveCellIfinfinite(viewModel: Models.SetInitialPlaybackCell.ViewModel) { playbackCollectionView.setContentOffset(.init(x: playbackCollectionView.contentOffset.x, y: playbackCollectionView.bounds.height * CGFloat(viewModel.indexPathRow)), animated: false) } @@ -334,6 +341,14 @@ extension PlaybackViewController: UICollectionViewDelegate { interactor?.playTeleportVideo(with: request) interactor?.careVideoLoading(with: request) } + + func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { + let currentOffset = scrollView.contentOffset.y + let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height + if maximumOffset < currentOffset { + interactor?.fetchPosts() + } + } } extension PlaybackViewController: PlaybackViewControllerDelegate { diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackWorker.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackWorker.swift index 456f90c..da91dba 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackWorker.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackWorker.swift @@ -16,6 +16,9 @@ protocol PlaybackWorkerProtocol { func makeInfiniteScroll(posts: [Post]) -> [Post] func transLocation(latitude: Double, longitude: Double) async -> String? func fetchImageData(with url: URL?) async -> Data? + func fetchHomePosts() async -> [Post]? + func fetchProfilePosts(profileID: Int?, page: Int) async -> [Post]? + func fetchTagPosts(selectedTag: String, page: Int) async -> [Post]? } final class PlaybackWorker: PlaybackWorkerProtocol { @@ -26,12 +29,16 @@ final class PlaybackWorker: PlaybackWorkerProtocol { private let provider: ProviderType private let defaultPostManagerEndPointFactory: PostManagerEndPointFactory + private let defaultPostEndPointFactory: PostEndPointFactory + private let defaultUserEndPointFactory: UserEndPointFactory // MARK: - Methods - init(provider: ProviderType = Provider(), defaultPostManagerEndPointFactory: PostManagerEndPointFactory = DefaultPostManagerEndPointFactory()) { + init(provider: ProviderType = Provider(), defaultPostManagerEndPointFactory: PostManagerEndPointFactory = DefaultPostManagerEndPointFactory(), defaultPostEndPointFactory: PostEndPointFactory = DefaultPostEndPointFactory(), defaultUserEndPointFactory: UserEndPointFactory = DefaultUserEndPointFactory()) { self.provider = provider self.defaultPostManagerEndPointFactory = defaultPostManagerEndPointFactory + self.defaultPostEndPointFactory = defaultPostEndPointFactory + self.defaultUserEndPointFactory = defaultUserEndPointFactory } func makeInfiniteScroll(posts: [Post]) -> [Post] { @@ -78,4 +85,37 @@ final class PlaybackWorker: PlaybackWorkerProtocol { return nil } } + + func fetchHomePosts() async -> [Post]? { + let endPoint = defaultPostEndPointFactory.makeHomePostListEndPoint() + do { + let response = try await provider.request(with: endPoint) + return response.data?.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) + return nil + } + } + + func fetchProfilePosts(profileID: Int?, page: Int) async -> [Post]? { + let endPoint = defaultUserEndPointFactory.makeUserPostsEndPoint(at: page, of: profileID) + do { + let response = try await provider.request(with: endPoint) + return response.data?.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) + return nil + } + } + + func fetchTagPosts(selectedTag: String, page: Int) async -> [Post]? { + let endPoint = defaultPostEndPointFactory.makeTagSearchPostListEndPoint(of: selectedTag, at: page) + do { + let response = try await provider.request(with: endPoint) + return response.data?.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) + return nil + } + } } diff --git a/iOS/Layover/Layover/Scenes/Profile/ProfileRouter.swift b/iOS/Layover/Layover/Scenes/Profile/ProfileRouter.swift index 3dacd61..f81ba6d 100644 --- a/iOS/Layover/Layover/Scenes/Profile/ProfileRouter.swift +++ b/iOS/Layover/Layover/Scenes/Profile/ProfileRouter.swift @@ -64,6 +64,6 @@ final class ProfileRouter: ProfileRoutingLogic, ProfileDataPassing { private func passDataToPlayback(source: ProfileDataStore, destination: inout PlaybackDataStore) { destination.posts = source.posts destination.index = source.playbackStartIndex - destination.parentView = .myProfile + destination.parentView = source.profileId == nil ? .myProfile : .otherProfile } } diff --git a/iOS/Layover/Layover/Scenes/TagPlayList/TagPlayListRouter.swift b/iOS/Layover/Layover/Scenes/TagPlayList/TagPlayListRouter.swift index 797a48f..ad40750 100644 --- a/iOS/Layover/Layover/Scenes/TagPlayList/TagPlayListRouter.swift +++ b/iOS/Layover/Layover/Scenes/TagPlayList/TagPlayListRouter.swift @@ -33,7 +33,7 @@ final class TagPlayListRouter: TagPlayListRoutingLogic, TagPlayListDataPassing { } private func passDataToPlayback(source: TagPlayListDataStore, destination: inout PlaybackDataStore) { - destination.parentView = .other + destination.parentView = .tag destination.index = source.postPlayStartIndex destination.posts = source.posts } diff --git a/iOS/Layover/Layover/Workers/Mocks/MockPlaybackWorker.swift b/iOS/Layover/Layover/Workers/Mocks/MockPlaybackWorker.swift index ec23402..ca84a80 100644 --- a/iOS/Layover/Layover/Workers/Mocks/MockPlaybackWorker.swift +++ b/iOS/Layover/Layover/Workers/Mocks/MockPlaybackWorker.swift @@ -100,4 +100,80 @@ final class MockPlaybackWorker: PlaybackWorkerProtocol { return nil } } + + func fetchHomePosts() async -> [Post]? { + guard let fileLocation = Bundle.main.url(forResource: "PostList", + withExtension: "json") else { return nil } + + do { + let mockData = try Data(contentsOf: fileLocation) + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + let endPoint: EndPoint = EndPoint<Response<[PostDTO]>>(path: "/board/home", + method: .GET) + let response = try await provider.request(with: endPoint) + guard let data = response.data else { return nil } + return data.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "%@", error.localizedDescription) + return nil + } + } + + func fetchProfilePosts(profileID: Int?, page: Int) async -> [Post]? { + let resourceFileName = switch page { case 1: "PostList" case 2: "PostListMore" default: "PostListEnd" } + guard let fileLocation = Bundle.main.url(forResource: resourceFileName, withExtension: "json") else { return nil } + do { + let mockData = try Data(contentsOf: fileLocation) + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + let endPoint = EndPoint<Response<[PostDTO]>>(path: "/member/posts", + method: .GET, + queryParameters: ["page": page]) + let response = try await provider.request(with: endPoint) + return response.data?.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "%@", error.localizedDescription) + return nil + } + } + + func fetchTagPosts(selectedTag: String, page: Int) async -> [Post]? { + let resourceFileName = switch page { case 1: "PostList" case 2: "PostListMore" default: "PostListEnd" } + guard let fileLocation = Bundle.main.url(forResource: resourceFileName, withExtension: "json") else { + return nil + } + + do { + let mockData = try? Data(contentsOf: fileLocation) + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + + let endPoint = EndPoint<Response<[PostDTO]>>(path: "/board/tag", + method: .GET, + queryParameters: ["tag": selectedTag]) + + let response = try await provider.request(with: endPoint) + return response.data?.map { $0.toDomain() } + } catch { + os_log(.error, log: .data, "%@", error.localizedDescription) + return nil + } + } + }