diff --git a/Projects/App/Sources/Application/AppComponent+Playlist.swift b/Projects/App/Sources/Application/AppComponent+Playlist.swift index e6534e14a..10c52b256 100644 --- a/Projects/App/Sources/Application/AppComponent+Playlist.swift +++ b/Projects/App/Sources/Application/AppComponent+Playlist.swift @@ -80,6 +80,12 @@ public extension AppComponent { } } + var fetchWmPlaylistDetailUseCase: any FetchWmPlaylistDetailUseCase { + shared { + FetchWmPlaylistDetailUseCaseImpl(playlistRepository: playlistRepository) + } + } + var createPlaylistUseCase: any CreatePlaylistUseCase { shared { CreatePlaylistUseCaseImpl(playlistRepository: playlistRepository) diff --git a/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift b/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift index e59932829..86b7d46bc 100644 --- a/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift +++ b/Projects/Domains/PlaylistDomain/Interface/DataSource/RemotePlaylistDataSource.swift @@ -6,6 +6,7 @@ import SongsDomainInterface public protocol RemotePlaylistDataSource { func fetchRecommendPlaylist() -> Single<[RecommendPlaylistEntity]> func fetchPlaylistDetail(id: String, type: PlaylistType) -> Single + func fetchWmPlaylistDetail(id: String) -> Single func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable func createPlaylist(title: String) -> Single func fetchPlaylistSongs(key: String) -> Single<[SongEntity]> diff --git a/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift b/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift new file mode 100644 index 000000000..7deabaaef --- /dev/null +++ b/Projects/Domains/PlaylistDomain/Interface/Entity/WmPlaylistDetailEntity.swift @@ -0,0 +1,21 @@ +import Foundation +import SongsDomainInterface + +public struct WmPlaylistDetailEntity: Equatable { + public init( + key: String, + title: String, + songs: [SongEntity], + image: String, + playlistURL: String + ) { + self.key = key + self.title = title + self.songs = songs + self.image = image + self.playlistURL = playlistURL + } + + public let key, title, image, playlistURL: String + public var songs: [SongEntity] +} diff --git a/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift b/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift index c1a5cd8ee..4c73b5446 100644 --- a/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift +++ b/Projects/Domains/PlaylistDomain/Interface/Repository/PlaylistRepository.swift @@ -6,6 +6,7 @@ import SongsDomainInterface public protocol PlaylistRepository { func fetchRecommendPlaylist() -> Single<[RecommendPlaylistEntity]> func fetchPlaylistDetail(id: String, type: PlaylistType) -> Single + func fetchWmPlaylistDetail(id: String) -> Single func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable func createPlaylist(title: String) -> Single func fetchPlaylistSongs(key: String) -> Single<[SongEntity]> diff --git a/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift new file mode 100644 index 000000000..bc9dfad58 --- /dev/null +++ b/Projects/Domains/PlaylistDomain/Interface/UseCase/FetchWmPlaylistDetailUseCase.swift @@ -0,0 +1,6 @@ +import Foundation +import RxSwift + +public protocol FetchWmPlaylistDetailUseCase { + func execute(id: String) -> Single +} diff --git a/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift b/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift index adc218984..f00ea811f 100644 --- a/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift +++ b/Projects/Domains/PlaylistDomain/Sources/API/PlaylistAPI.swift @@ -8,6 +8,7 @@ import PlaylistDomainInterface public enum PlaylistAPI { case fetchPlaylistDetail(id: String, type: PlaylistType) // 플리 상세 불러오기 + case fetchWmPlaylistDetail(id: String) // 왁뮤 플리 상세 불러오기 case updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) // title and private 업데이트 case createPlaylist(title: String) // 플리 생성 case fetchPlaylistSongs(key: String) // 전체 재생 시 곡 데이터만 가져오기 @@ -33,12 +34,10 @@ extension PlaylistAPI: WMAPI { return "/recommend/list" case let .fetchPlaylistDetail(id: id, type: type): - switch type { - case .unknown, .my: - return "/\(id)" - case .wmRecommend: - return "/recommend/\(id)" - } + return "/\(id)" + + case let .fetchWmPlaylistDetail(id: id): + return "/recommend/\(id)" case let .updateTitleAndPrivate(key: key, _, _): return "/\(key)" @@ -65,7 +64,8 @@ extension PlaylistAPI: WMAPI { public var method: Moya.Method { switch self { - case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchPlaylistSongs, .checkSubscription, + case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWmPlaylistDetail, .fetchPlaylistSongs, + .checkSubscription, .requestPlaylistOwnerID: return .get @@ -85,7 +85,8 @@ extension PlaylistAPI: WMAPI { public var task: Moya.Task { switch self { - case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchPlaylistSongs, .subscribePlaylist, .checkSubscription, + case .fetchRecommendPlaylist, .fetchPlaylistDetail, .fetchWmPlaylistDetail, .fetchPlaylistSongs, + .subscribePlaylist, .checkSubscription, .requestPlaylistOwnerID: return .requestPlain @@ -124,7 +125,7 @@ extension PlaylistAPI: WMAPI { public var jwtTokenType: JwtTokenType { switch self { - case .fetchRecommendPlaylist: + case .fetchRecommendPlaylist, .fetchWmPlaylistDetail: return .none case let .fetchPlaylistDetail(_, type): diff --git a/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift b/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift index afe05c298..a96c06dc0 100644 --- a/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift +++ b/Projects/Domains/PlaylistDomain/Sources/DataSource/RemotePlaylistDataSourceImpl.swift @@ -22,6 +22,12 @@ public final class RemotePlaylistDataSourceImpl: BaseRemoteDataSource Single { + request(.fetchWmPlaylistDetail(id: id)) + .map(WmPlaylistDetailResponseDTO.self) + .map { $0.toDomain() } + } + public func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable { request(.updateTitleAndPrivate(key: key, title: title, isPrivate: isPrivate)) .asCompletable() diff --git a/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift b/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift index 7f19d4b69..04a6418e8 100644 --- a/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift +++ b/Projects/Domains/PlaylistDomain/Sources/Repository/PlaylistRepositoryImpl.swift @@ -21,6 +21,10 @@ public final class PlaylistRepositoryImpl: PlaylistRepository { remotePlaylistDataSource.fetchPlaylistDetail(id: id, type: type) } + public func fetchWmPlaylistDetail(id: String) -> Single { + remotePlaylistDataSource.fetchWmPlaylistDetail(id: id) + } + public func updateTitleAndPrivate(key: String, title: String?, isPrivate: Bool?) -> Completable { remotePlaylistDataSource.updateTitleAndPrivate(key: key, title: title, isPrivate: isPrivate) } diff --git a/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift b/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift new file mode 100644 index 000000000..b5631a1a3 --- /dev/null +++ b/Projects/Domains/PlaylistDomain/Sources/ResponseDTO/WmPlaylistDetailResponseDTO.swift @@ -0,0 +1,32 @@ +import Foundation +import PlaylistDomainInterface +import SongsDomain +import SongsDomainInterface + +public struct WmPlaylistDetailResponseDTO: Decodable { + public let key: String? + public let title: String + public let songs: [SingleSongResponseDTO]? + public let imageURL: String + public let playlistURL: String + + enum CodingKeys: String, CodingKey { + case key + case title + case songs + case imageURL = "imageUrl" + case playlistURL = "playlistUrl" + } +} + +public extension WmPlaylistDetailResponseDTO { + func toDomain() -> WmPlaylistDetailEntity { + WmPlaylistDetailEntity( + key: key ?? "", + title: title, + songs: (songs ?? []).map { $0.toDomain() }, + image: imageURL, + playlistURL: playlistURL + ) + } +} diff --git a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchPlaylistDetailUseCaseImpl.swift b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchPlaylistDetailUseCaseImpl.swift index ceb2cd73a..cc84d35e5 100644 --- a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchPlaylistDetailUseCaseImpl.swift +++ b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchPlaylistDetailUseCaseImpl.swift @@ -1,11 +1,3 @@ -// -// FetchArtistListUseCaseImpl.swift -// DataModule -// -// Created by KTH on 2023/02/08. -// Copyright © 2023 yongbeomkwak. All rights reserved. -// - import Foundation import PlaylistDomainInterface import RxSwift diff --git a/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift new file mode 100644 index 000000000..7c1eb818c --- /dev/null +++ b/Projects/Domains/PlaylistDomain/Sources/UseCase/FetchWmPlaylistDetailUseCaseImpl.swift @@ -0,0 +1,17 @@ +import Foundation +import PlaylistDomainInterface +import RxSwift + +public struct FetchWmPlaylistDetailUseCaseImpl: FetchWmPlaylistDetailUseCase { + private let playlistRepository: any PlaylistRepository + + public init( + playlistRepository: PlaylistRepository + ) { + self.playlistRepository = playlistRepository + } + + public func execute(id: String) -> Single { + playlistRepository.fetchWmPlaylistDetail(id: id) + } +} diff --git a/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift b/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift index 2d57e4a19..9b95c8e62 100644 --- a/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift +++ b/Projects/Features/PlaylistFeature/Sources/Components/WakmusicPlaylistDetailComponent.swift @@ -8,7 +8,7 @@ import SignInFeatureInterface import UIKit public protocol WakmusicPlaylistDetailDependency: Dependency { - var fetchPlaylistDetailUseCase: any FetchPlaylistDetailUseCase { get } + var fetchWmPlaylistDetailUseCase: any FetchWmPlaylistDetailUseCase { get } var containSongsFactory: any ContainSongsFactory { get } var textPopUpFactory: any TextPopUpFactory { get } var songDetailPresenter: any SongDetailPresentable { get } @@ -21,7 +21,7 @@ public final class WakmusicPlaylistDetailComponent: Component Observable { return .concat([ .just(.updateLoadingState(true)), - fetchPlaylistDetailUseCase.execute(id: key, type: .wmRecommend) + fetchWmPlaylistDetailUseCase.execute(id: key) .asObservable() .flatMap { data -> Observable in return .concat([ @@ -125,11 +130,12 @@ private extension WakmusicPlaylistDetailReactor { key: data.key, title: data.title, image: data.image, - userName: data.userName, - private: data.private, + userName: "Wakmu", + private: false, songCount: data.songs.count ) )), + Observable.just(.updatePlaylistURL(data.playlistURL)), Observable.just(Mutation.updateDataSource(data.songs)) ]) } diff --git a/Projects/Features/PlaylistFeature/Sources/ViewControllers/WakmusicPlaylistDetailViewController.swift b/Projects/Features/PlaylistFeature/Sources/ViewControllers/WakmusicPlaylistDetailViewController.swift index d2d1f07ea..35d326fd5 100644 --- a/Projects/Features/PlaylistFeature/Sources/ViewControllers/WakmusicPlaylistDetailViewController.swift +++ b/Projects/Features/PlaylistFeature/Sources/ViewControllers/WakmusicPlaylistDetailViewController.swift @@ -274,8 +274,9 @@ extension WakmusicPlaylistDetailViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let playbuttonGroupView = PlayButtonGroupView() - playbuttonGroupView.delegate = self + let view = SingleActionButtonView(frame: CGRect(x: 0, y: 0, width: APP_WIDTH(), height: 78)) + view.delegate = self + view.setTitleAndImage(text: "전체재생", image: DesignSystemAsset.Chart.allPlay.image) guard let reactor = reactor else { return nil @@ -284,7 +285,7 @@ extension WakmusicPlaylistDetailViewController: UITableViewDelegate { if reactor.currentState.dataSource.isEmpty { return nil } else { - return playbuttonGroupView + return view } } @@ -305,6 +306,22 @@ extension WakmusicPlaylistDetailViewController: UITableViewDelegate { } } +extension WakmusicPlaylistDetailViewController: SingleActionButtonViewDelegate { + func tappedButtonAction() { + guard let currentState = self.reactor?.currentState, let playlistURL = currentState.playlistURL, + let url = URL(string: playlistURL) else { + showToast(text: "해당 기능은 준비 중입니다.", options: [.tabBar]) + return + } + + let songs = currentState.dataSource + + LogManager.analytics(PlaylistAnalyticsLog.clickPlaylistPlayButton(type: "all", key: reactor?.key ?? "")) + PlayState.shared.append(contentsOf: songs.map { PlaylistItem(item: $0) }) + UIApplication.shared.open(url) + } +} + extension WakmusicPlaylistDetailViewController: PlaylistDateTableViewCellDelegate { func thumbnailDidTap(key: String) { guard let tappedSong = reactor?.currentState.dataSource @@ -315,36 +332,6 @@ extension WakmusicPlaylistDetailViewController: PlaylistDateTableViewCellDelegat } } -/// 전체재생 , 랜덤 재생 델리게이트 -extension WakmusicPlaylistDetailViewController: PlayButtonGroupViewDelegate { - func play(_ event: PlayEvent) { - guard let reactor = reactor else { - return - } - - let currentState = reactor.currentState - var songs = currentState.dataSource - - switch event { - case .allPlay: - LogManager.analytics( - CommonAnalyticsLog.clickPlayButton(location: .playlistDetail, type: .all) - ) - LogManager.analytics(PlaylistAnalyticsLog.clickPlaylistPlayButton(type: "all", key: reactor.key)) - - case .shufflePlay: - LogManager.analytics( - CommonAnalyticsLog.clickPlayButton(location: .playlistDetail, type: .random) - ) - LogManager.analytics(PlaylistAnalyticsLog.clickPlaylistPlayButton(type: "random", key: reactor.key)) - songs.shuffle() - } - - PlayState.shared.append(contentsOf: songs.map { PlaylistItem(item: $0) }) - WakmusicYoutubePlayer(ids: songs.map { $0.id }).play() - } -} - /// 송카트 델리게이트 extension WakmusicPlaylistDetailViewController: SongCartViewDelegate { func buttonTapped(type: SongCartSelectType) { @@ -354,6 +341,7 @@ extension WakmusicPlaylistDetailViewController: SongCartViewDelegate { let currentState = reactor.currentState let songs = currentState.dataSource.filter { $0.isSelected } + let limit = 50 switch type { case let .allSelect(flag: flag): @@ -366,6 +354,14 @@ extension WakmusicPlaylistDetailViewController: SongCartViewDelegate { let log = CommonAnalyticsLog.clickAddMusicsButton(location: .playlistDetail) LogManager.analytics(log) + guard songs.count <= limit else { + showToast( + text: LocalizationStrings.overFlowContainWarning(songs.count - limit), + options: [.tabBar, .songCart] + ) + return + } + if PreferenceManager.userInfo == nil { reactor.action.onNext(.requestLoginRequiredAction) return @@ -377,6 +373,7 @@ extension WakmusicPlaylistDetailViewController: SongCartViewDelegate { reactor.action.onNext(.deselectAll) case .addPlayList: + PlayState.shared.append(contentsOf: songs.map { PlaylistItem(item: $0) }) reactor.action.onNext(.deselectAll) showToast( @@ -385,6 +382,14 @@ extension WakmusicPlaylistDetailViewController: SongCartViewDelegate { ) case .play: + + guard songs.count <= limit else { + showToast( + text: LocalizationStrings.overFlowPlayWarning(songs.count - limit), + options: [.tabBar, .songCart] + ) + return + } LogManager.analytics( CommonAnalyticsLog.clickPlayButton(location: .playlistDetail, type: .multiple) ) diff --git a/Projects/Features/SearchFeature/Sources/After/ViewControllers/SongSearchResultViewController.swift b/Projects/Features/SearchFeature/Sources/After/ViewControllers/SongSearchResultViewController.swift index 75d8784d0..7258d6793 100644 --- a/Projects/Features/SearchFeature/Sources/After/ViewControllers/SongSearchResultViewController.swift +++ b/Projects/Features/SearchFeature/Sources/After/ViewControllers/SongSearchResultViewController.swift @@ -346,7 +346,7 @@ extension SongSearchResultViewController: SongCartViewDelegate { guard songs.count <= limit else { showToast( text: LocalizationStrings.overFlowContainWarning(songs.count - limit), - options: [.tabBar] + options: [.tabBar, .songCart] ) return } @@ -380,8 +380,8 @@ extension SongSearchResultViewController: SongCartViewDelegate { guard songs.count <= limit else { showToast( - text: LocalizationStrings.overFlowContainWarning(songs.count - limit), - options: [.tabBar] + text: LocalizationStrings.overFlowAddPlaylistWarning(songs.count - limit), + options: [.tabBar, .songCart] ) return } @@ -399,7 +399,7 @@ extension SongSearchResultViewController: SongCartViewDelegate { guard songs.count <= limit else { showToast( text: LocalizationStrings.overFlowPlayWarning(songs.count - limit), - options: [.tabBar] + options: [.tabBar, .songCart] ) return }