diff --git a/StreetDrop/StreetDrop.xcodeproj/project.pbxproj b/StreetDrop/StreetDrop.xcodeproj/project.pbxproj index d9b85caf..67810342 100644 --- a/StreetDrop/StreetDrop.xcodeproj/project.pbxproj +++ b/StreetDrop/StreetDrop.xcodeproj/project.pbxproj @@ -210,7 +210,7 @@ C45BF3A62A1D19C600CEDE74 /* UserDefaultKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BF3962A1D0ADF00CEDE74 /* UserDefaultKey.swift */; }; C45BF3A72A1D19CC00CEDE74 /* RecentMusicQueriesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BF3A22A1D179C00CEDE74 /* RecentMusicQueriesStorage.swift */; }; C45BF3A82A1D19D100CEDE74 /* RecentMusicQueryDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BF3A02A1D133000CEDE74 /* RecentMusicQueryDTO.swift */; }; - C45BF3AC2A1EF64300CEDE74 /* RecentQueryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BF3AB2A1EF64300CEDE74 /* RecentQueryButton.swift */; }; + C45BF3AC2A1EF64300CEDE74 /* RecentQueryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BF3AB2A1EF64300CEDE74 /* RecentQueryCell.swift */; }; C46410F42A629820009DD88F /* MusicAppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46410F32A629820009DD88F /* MusicAppButton.swift */; }; C4685B362B725FF500F514C7 /* UserCircleRadiusResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4685B352B725FF500F514C7 /* UserCircleRadiusResponseDTO.swift */; }; C4685B382B72605500F514C7 /* FetchingUserCircleRadiusUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4685B372B72605500F514C7 /* FetchingUserCircleRadiusUsecase.swift */; }; @@ -258,6 +258,14 @@ C51230A18096B11C82137084 /* libPods-StreetDrop.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642A4AE13B198B9F5F5F76E4 /* libPods-StreetDrop.a */; }; F48DF73A2C1DD8F500F6DEA1 /* SettingPushNotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48DF7392C1DD8F500F6DEA1 /* SettingPushNotificationCell.swift */; }; F48DF73C2C1DD91C00F6DEA1 /* SettingMusicSelectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48DF73B2C1DD91C00F6DEA1 /* SettingMusicSelectCell.swift */; }; + F49CC9F22C638691007484EE /* RecommendMusicUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49CC9F12C638691007484EE /* RecommendMusicUsecase.swift */; }; + F49CC9F62C638CCF007484EE /* DefaultRecommendMusicRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49CC9F52C638CCF007484EE /* DefaultRecommendMusicRepository.swift */; }; + F49CC9F82C638DC6007484EE /* RecommendMusicRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49CC9F72C638DC6007484EE /* RecommendMusicRepository.swift */; }; + F49CC9FC2C6411E5007484EE /* RecommendMusicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49CC9FB2C6411E5007484EE /* RecommendMusicCell.swift */; }; + F49CC9FE2C6411F9007484EE /* RecommendArtistCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49CC9FD2C6411F9007484EE /* RecommendArtistCell.swift */; }; + F49CCA002C667111007484EE /* RecommendHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49CC9FF2C667111007484EE /* RecommendHeaderView.swift */; }; + F49DB0B42C6E6E3400686E5F /* RecommendMusicListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49DB0B32C6E6E3400686E5F /* RecommendMusicListViewController.swift */; }; + F49DB0B62C6E6F2100686E5F /* RecommendMusicListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49DB0B52C6E6F2100686E5F /* RecommendMusicListViewModel.swift */; }; F4AA84D92C1F030F00CADB1A /* NoticeListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AA84D82C1F030F00CADB1A /* NoticeListResponseDTO.swift */; }; F4AA84DB2C1F106300CADB1A /* NoticeDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AA84DA2C1F106300CADB1A /* NoticeDetailViewController.swift */; }; F4AA84DD2C1F10FF00CADB1A /* NoticeDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AA84DC2C1F10FF00CADB1A /* NoticeDetailViewModel.swift */; }; @@ -271,6 +279,7 @@ F4C996AA2C1EEF0B00FF7B9A /* DefaultNoticeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C996A92C1EEF0B00FF7B9A /* DefaultNoticeRepository.swift */; }; F4C996AD2C1EEF2500FF7B9A /* NoticeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C996AC2C1EEF2500FF7B9A /* NoticeRepository.swift */; }; F4C996AF2C1EF6C100FF7B9A /* Notice.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C996AE2C1EF6C100FF7B9A /* Notice.swift */; }; + F4CBD7BE2C6A534600A5FD91 /* RecentSearchesHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CBD7BD2C6A534600A5FD91 /* RecentSearchesHeaderView.swift */; }; F4CBD7C02C6C181900A5FD91 /* GuideDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CBD7BF2C6C181900A5FD91 /* GuideDetailView.swift */; }; /* End PBXBuildFile section */ @@ -470,7 +479,7 @@ C45BF39D2A1D113200CEDE74 /* UserDefaultsRecentMusicSearches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsRecentMusicSearches.swift; sourceTree = ""; }; C45BF3A02A1D133000CEDE74 /* RecentMusicQueryDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentMusicQueryDTO.swift; sourceTree = ""; }; C45BF3A22A1D179C00CEDE74 /* RecentMusicQueriesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentMusicQueriesStorage.swift; sourceTree = ""; }; - C45BF3AB2A1EF64300CEDE74 /* RecentQueryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentQueryButton.swift; sourceTree = ""; }; + C45BF3AB2A1EF64300CEDE74 /* RecentQueryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentQueryCell.swift; sourceTree = ""; }; C46410F32A629820009DD88F /* MusicAppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicAppButton.swift; sourceTree = ""; }; C4685B352B725FF500F514C7 /* UserCircleRadiusResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCircleRadiusResponseDTO.swift; sourceTree = ""; }; C4685B372B72605500F514C7 /* FetchingUserCircleRadiusUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchingUserCircleRadiusUsecase.swift; sourceTree = ""; }; @@ -513,6 +522,14 @@ C4E68C842C2A996000742464 /* AsynchronousError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousError.swift; sourceTree = ""; }; F48DF7392C1DD8F500F6DEA1 /* SettingPushNotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingPushNotificationCell.swift; sourceTree = ""; }; F48DF73B2C1DD91C00F6DEA1 /* SettingMusicSelectCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingMusicSelectCell.swift; sourceTree = ""; }; + F49CC9F12C638691007484EE /* RecommendMusicUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendMusicUsecase.swift; sourceTree = ""; }; + F49CC9F52C638CCF007484EE /* DefaultRecommendMusicRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultRecommendMusicRepository.swift; sourceTree = ""; }; + F49CC9F72C638DC6007484EE /* RecommendMusicRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendMusicRepository.swift; sourceTree = ""; }; + F49CC9FB2C6411E5007484EE /* RecommendMusicCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendMusicCell.swift; sourceTree = ""; }; + F49CC9FD2C6411F9007484EE /* RecommendArtistCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendArtistCell.swift; sourceTree = ""; }; + F49CC9FF2C667111007484EE /* RecommendHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHeaderView.swift; sourceTree = ""; }; + F49DB0B32C6E6E3400686E5F /* RecommendMusicListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendMusicListViewController.swift; sourceTree = ""; }; + F49DB0B52C6E6F2100686E5F /* RecommendMusicListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendMusicListViewModel.swift; sourceTree = ""; }; F4AA84D82C1F030F00CADB1A /* NoticeListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeListResponseDTO.swift; sourceTree = ""; }; F4AA84DA2C1F106300CADB1A /* NoticeDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeDetailViewController.swift; sourceTree = ""; }; F4AA84DC2C1F10FF00CADB1A /* NoticeDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeDetailViewModel.swift; sourceTree = ""; }; @@ -526,6 +543,7 @@ F4C996A92C1EEF0B00FF7B9A /* DefaultNoticeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNoticeRepository.swift; sourceTree = ""; }; F4C996AC2C1EEF2500FF7B9A /* NoticeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRepository.swift; sourceTree = ""; }; F4C996AE2C1EF6C100FF7B9A /* Notice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notice.swift; sourceTree = ""; }; + F4CBD7BD2C6A534600A5FD91 /* RecentSearchesHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSearchesHeaderView.swift; sourceTree = ""; }; F4CBD7BF2C6C181900A5FD91 /* GuideDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideDetailView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -646,6 +664,7 @@ 04FB1F292A021B750064B3C8 /* Protocol */, 18683FDA2A348B00005A94AC /* Main */, C434A4D52A17984200C63526 /* SearchingMusic */, + F49CC9F42C638CB8007484EE /* RecommendMusic */, C4A445912A5EF24E008279C1 /* FCM */, C4E4C6B52A5ADC4F00B1C84A /* Setting */, 1816ED472A685902005009FC /* MyPage */, @@ -756,6 +775,7 @@ C4BB15992A5EC50D001BC5E8 /* FCM */, 18683FDD2A348B19005A94AC /* Main */, C434A4D62A17986400C63526 /* SearchingMusic */, + F49CC9F32C638CAF007484EE /* RecommendMusic */, C4E4C6B02A5ADBF200B1C84A /* Setting */, 1816ED442A6858C2005009FC /* MyPage */, 41F23B4D2A1B6EE80083D2EC /* DropMusicRepository.swift */, @@ -836,6 +856,7 @@ 6AAFD9AE2BCE567A001A6772 /* FetchingLevelPolicyUseCase.swift */, 6AAFD9B02BCE56A6001A6772 /* DefaultFetchingLevelPolicyUseCase.swift */, F4C996A62C1EEC8600FF7B9A /* NoticeUseCase.swift */, + F49CC9F12C638691007484EE /* RecommendMusicUsecase.swift */, ); path = UseCase; sourceTree = ""; @@ -1248,9 +1269,15 @@ children = ( C434A4C92A1796E400C63526 /* SearchingMusicViewController.swift */, C434A4DB2A19CA6F00C63526 /* SearchingMusicTableViewCell.swift */, - C45BF3AB2A1EF64300CEDE74 /* RecentQueryButton.swift */, + C45BF3AB2A1EF64300CEDE74 /* RecentQueryCell.swift */, B4B9EE852ADEB2AC000A6507 /* RecommendKeywordItemCell.swift */, B46578C22B00BC060024B066 /* LeftAlignedCollectionViewFlowLayout.swift */, + F49CC9FB2C6411E5007484EE /* RecommendMusicCell.swift */, + F49CC9FD2C6411F9007484EE /* RecommendArtistCell.swift */, + F49CC9FF2C667111007484EE /* RecommendHeaderView.swift */, + F4CBD7BD2C6A534600A5FD91 /* RecentSearchesHeaderView.swift */, + F4CBD7BF2C6C181900A5FD91 /* GuideDetailView.swift */, + F49DB0B32C6E6E3400686E5F /* RecommendMusicListViewController.swift */, ); path = View; sourceTree = ""; @@ -1259,6 +1286,7 @@ isa = PBXGroup; children = ( C434A4CB2A1796F300C63526 /* SearchingMusicViewModel.swift */, + F49DB0B52C6E6F2100686E5F /* RecommendMusicListViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -1510,6 +1538,22 @@ path = Pods; sourceTree = ""; }; + F49CC9F32C638CAF007484EE /* RecommendMusic */ = { + isa = PBXGroup; + children = ( + F49CC9F72C638DC6007484EE /* RecommendMusicRepository.swift */, + ); + path = RecommendMusic; + sourceTree = ""; + }; + F49CC9F42C638CB8007484EE /* RecommendMusic */ = { + isa = PBXGroup; + children = ( + F49CC9F52C638CCF007484EE /* DefaultRecommendMusicRepository.swift */, + ); + path = RecommendMusic; + sourceTree = ""; + }; F4AA84D72C1F030300CADB1A /* Notice */ = { isa = PBXGroup; children = ( @@ -1805,6 +1849,7 @@ F4C996A52C1EEC3600FF7B9A /* NoticeListViewModel.swift in Sources */, 41F23B4E2A1B6EE80083D2EC /* DropMusicRepository.swift in Sources */, C4685B382B72605500F514C7 /* FetchingUserCircleRadiusUsecase.swift in Sources */, + F49CC9F62C638CCF007484EE /* DefaultRecommendMusicRepository.swift in Sources */, 18683FE12A349109005A94AC /* PoiEntity.swift in Sources */, 6AAFD9AF2BCE567A001A6772 /* FetchingLevelPolicyUseCase.swift in Sources */, 1876F03D2A66E0330064B887 /* MyDropListResponseDTO+Mapping.swift in Sources */, @@ -1832,7 +1877,7 @@ 41396DA12A51B0DF00B69341 /* EditCommentViewController.swift in Sources */, C44A549A2BBC097E00354F8F /* FetchingPopUpInfomationUseCase.swift in Sources */, 41008EBA2A49B56F00FD4ABE /* ClaimModalViewController.swift in Sources */, - C45BF3AC2A1EF64300CEDE74 /* RecentQueryButton.swift in Sources */, + C45BF3AC2A1EF64300CEDE74 /* RecentQueryCell.swift in Sources */, C45BF3972A1D0ADF00CEDE74 /* UserDefaultKey.swift in Sources */, 41A3DDF12A593ED4004CFA2F /* AlertViewController.swift in Sources */, C47F02222A38633500F48884 /* SettingsViewController.swift in Sources */, @@ -1852,11 +1897,13 @@ C4685B362B725FF500F514C7 /* UserCircleRadiusResponseDTO.swift in Sources */, 040685002A01539800377094 /* AppDelegate.swift in Sources */, C47583A12A5F11BD00CA7335 /* Bundle+APIKeys.swift in Sources */, + F49DB0B62C6E6F2100686E5F /* RecommendMusicListViewModel.swift in Sources */, C472B1882AC5523300482B2D /* DroppingInfo.swift in Sources */, 1876F0442A66E6E00064B887 /* MyPageRepository.swift in Sources */, C449807E2BC3B09F0001E6C3 /* DefaultPostingPopUpUserReadingUseCase.swift in Sources */, C41972302ABDAB0200211222 /* DefaultMyInfoUseCase.swift in Sources */, 41396D8F2A4EFBE800B69341 /* DefaultDeleteMusicRepository.swift in Sources */, + F49CC9F82C638DC6007484EE /* RecommendMusicRepository.swift in Sources */, 41396D822A4EF65B00B69341 /* EditCommentRequestDTO.swift in Sources */, C434A4D32A17983A00C63526 /* SearchingMusicRepository.swift in Sources */, 41A3DDF82A5AD222004CFA2F /* DefaultBlockUserRepository.swift in Sources */, @@ -1866,6 +1913,7 @@ 1867C8202A4DDB8C00F8EC48 /* MyPageViewController.swift in Sources */, 1816ED432A685865005009FC /* NicknameEditModel.swift in Sources */, C40008EA2A32012B00EA7FA9 /* Music.swift in Sources */, + F49DB0B42C6E6E3400686E5F /* RecommendMusicListViewController.swift in Sources */, 413ABEEC2A39D1E900EA1010 /* UIcolor+.swift in Sources */, 41A9BEBB2A4DCBDA00F3605C /* DefaultClaimCommentRepository.swift in Sources */, 414139972A20B434005418A1 /* UIImageView+makeCircleShape.swift in Sources */, @@ -1890,6 +1938,7 @@ 6AAFD9AD2BCE55A4001A6772 /* LevelPolicy.swift in Sources */, C434A4D32A17983A00C63526 /* SearchingMusicRepository.swift in Sources */, C45BF39E2A1D113200CEDE74 /* UserDefaultsRecentMusicSearches.swift in Sources */, + F49CCA002C667111007484EE /* RecommendHeaderView.swift in Sources */, 6A1386AB2B4F8A5000E49BB5 /* String+Base64.swift in Sources */, 41396D8B2A4EFB2500B69341 /* EditCommentRepository.swift in Sources */, F4AA84E12C1F732800CADB1A /* DateManager.swift in Sources */, @@ -1937,6 +1986,7 @@ C41972572ABED40100211222 /* DefaultFetchingMyInfoUseCase.swift in Sources */, C419723A2ABDB50B00211222 /* DefaultFetchingMusicCountUseCase.swift in Sources */, F4C996AD2C1EEF2500FF7B9A /* NoticeRepository.swift in Sources */, + F49CC9FE2C6411F9007484EE /* RecommendArtistCell.swift in Sources */, F4AA84DB2C1F106300CADB1A /* NoticeDetailViewController.swift in Sources */, C47F02282A3864A500F48884 /* SettingElementCell.swift in Sources */, 1876F0472A66EDA20064B887 /* MyPageModel.swift in Sources */, @@ -1967,6 +2017,7 @@ 1876F0412A66E5440064B887 /* MyLevelResponseDTO+Mapping.swift in Sources */, C4685B3D2B7261A000F514C7 /* SplashViewModel.swift in Sources */, F4AA84DF2C1F3B0200CADB1A /* Array+Extension.swift in Sources */, + F49CC9FC2C6411E5007484EE /* RecommendMusicCell.swift in Sources */, 082F17062AB6DFEC00174D98 /* MusicDropUseCase.swift in Sources */, 18683FD92A2A251E005A94AC /* ViewModel.swift in Sources */, C41972442ABDC43C00211222 /* ClaimingCommentUseCase.swift in Sources */, @@ -1996,7 +2047,9 @@ C434A4CA2A1796E400C63526 /* SearchingMusicViewController.swift in Sources */, 188D2C7B2A1E448C0088F49C /* DroppedMusicWithinAreaCollectionViewCell.swift in Sources */, 41396D9B2A4F1C5600B69341 /* OptionModalViewController.swift in Sources */, + F4CBD7BE2C6A534600A5FD91 /* RecentSearchesHeaderView.swift in Sources */, C47C1D1F2A643C07007317EA /* SplashViewController.swift in Sources */, + F49CC9F22C638691007484EE /* RecommendMusicUsecase.swift in Sources */, C449807C2BC3B07E0001E6C3 /* PostingPopUpUserReadingUseCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/Protocol/RecentMusicQueriesStorage.swift b/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/Protocol/RecentMusicQueriesStorage.swift index c6e402fb..45ed96ce 100644 --- a/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/Protocol/RecentMusicQueriesStorage.swift +++ b/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/Protocol/RecentMusicQueriesStorage.swift @@ -16,6 +16,7 @@ protocol RecentMusicQueriesStorage { query: RecentMusicQueryDTO, completion: @escaping (Result) -> Void ) + func deleteRecentQuery(query: RecentMusicQueryDTO) async } protocol RecommendMusicQueriesStorage { diff --git a/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/RecentMusicSearch/UserDefaultsRecentMusicSearches.swift b/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/RecentMusicSearch/UserDefaultsRecentMusicSearches.swift index 05dbad2d..3521878f 100644 --- a/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/RecentMusicSearch/UserDefaultsRecentMusicSearches.swift +++ b/StreetDrop/StreetDrop/Data/PersistentStorages/UserDefaultsStorage/RecentMusicSearch/UserDefaultsRecentMusicSearches.swift @@ -68,6 +68,12 @@ extension UserDefaultsRecentMusicQueriesStorage: RecentMusicQueriesStorage { completion(.success(query)) } } + + func deleteRecentQuery(query: RecentMusicQueryDTO) async { + var queries = self.fetchRecentMusicQueries().list + self.cleanUpQueries(for: query, in: &queries) + self.persist(recentMusicQueries: queries) + } } diff --git a/StreetDrop/StreetDrop/Data/Repositories/Protocol/RecommendMusic/RecommendMusicRepository.swift b/StreetDrop/StreetDrop/Data/Repositories/Protocol/RecommendMusic/RecommendMusicRepository.swift new file mode 100644 index 00000000..e0c17469 --- /dev/null +++ b/StreetDrop/StreetDrop/Data/Repositories/Protocol/RecommendMusic/RecommendMusicRepository.swift @@ -0,0 +1,17 @@ +// +// RecommendMusicRepository.swift +// StreetDrop +// +// Created by jihye kim on 07/08/2024. +// + +import Foundation + +import RxSwift + +protocol RecommendMusicRepository { + func fetchPromptOfTheDay() -> Single + func fetchTrendingMusicList() -> Single<[Music]> + func fetchMostDroppedMusicList() -> Single<[Music]> + func fetchArtistList() -> Single<[Artist]> +} diff --git a/StreetDrop/StreetDrop/Data/Repositories/Protocol/SearchingMusic/SearchingMusicRepository.swift b/StreetDrop/StreetDrop/Data/Repositories/Protocol/SearchingMusic/SearchingMusicRepository.swift index 42935234..9a82f799 100644 --- a/StreetDrop/StreetDrop/Data/Repositories/Protocol/SearchingMusic/SearchingMusicRepository.swift +++ b/StreetDrop/StreetDrop/Data/Repositories/Protocol/SearchingMusic/SearchingMusicRepository.swift @@ -15,4 +15,5 @@ protocol SearchingMusicRepository { func fetchRecommendMusicQueries() -> Single func fetchRecentMusicQueries() -> Single<[String]> func fetchVillageName(latitude: Double, longitude: Double) -> Single + func deleteRecentMusicQueries(keyword: String) async } diff --git a/StreetDrop/StreetDrop/Data/Repositories/RecommendMusic/DefaultRecommendMusicRepository.swift b/StreetDrop/StreetDrop/Data/Repositories/RecommendMusic/DefaultRecommendMusicRepository.swift new file mode 100644 index 00000000..e2f296e6 --- /dev/null +++ b/StreetDrop/StreetDrop/Data/Repositories/RecommendMusic/DefaultRecommendMusicRepository.swift @@ -0,0 +1,67 @@ +// +// DefaultRecommendMusicRepository.swift +// StreetDrop +// +// Created by jihye kim on 07/08/2024. +// + +import Foundation + +import Moya +import RxSwift + +final class DefaultRecommendMusicRepository: RecommendMusicRepository { + private let networkManager: NetworkManager + + init( + networkManager: NetworkManager = NetworkManager( + provider: MoyaProvider() + ) + ) { + self.networkManager = networkManager + } + + // TODO: jihye - update api + + func fetchPromptOfTheDay() -> Single { + Single.just("비오는 날, 어떤 음악이 떠오르시나요?") + } + + func fetchTrendingMusicList() -> Single<[Music]> { + Single.just([ + Music( + albumName: "", + artistName: "ILLIT", + songName: "Magnetic", + durationTime: "2:41", + albumImage: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/300x300bb.jpg", + albumThumbnailImage: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg", + genre: [] + ) + ]) + } + + func fetchMostDroppedMusicList() -> Single<[Music]> { + Single.just([ + Music( + albumName: "", + artistName: "NewJeans", + songName: "Supernatural", + durationTime: "3:11", + albumImage: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/300x300bb.jpg", + albumThumbnailImage: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg", + genre: [] + ) + ]) + } + + func fetchArtistList() -> Single<[Artist]> { + return Single.just([ + Artist(name: "NewJeans", image: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg"), + Artist(name: "SEVENTEEN", image: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg"), + Artist(name: "아이유", image: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg"), + Artist(name: "aespa", image: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg"), + Artist(name: "최유리", image: "https://is2-ssl.mzstatic.com/image/thumb/Music126/v4/03/8d/0e/038d0e52-e96d-f386-b8eb-9f77fa013543/195497146918_Cover.jpg/100x100bb.jpg") + ]) + } +} diff --git a/StreetDrop/StreetDrop/Data/Repositories/SearchingMusic/DefaultSearchingMusicRepository.swift b/StreetDrop/StreetDrop/Data/Repositories/SearchingMusic/DefaultSearchingMusicRepository.swift index f171b776..7ac79c5c 100644 --- a/StreetDrop/StreetDrop/Data/Repositories/SearchingMusic/DefaultSearchingMusicRepository.swift +++ b/StreetDrop/StreetDrop/Data/Repositories/SearchingMusic/DefaultSearchingMusicRepository.swift @@ -80,4 +80,10 @@ final class DefaultSearchingMusicRepository: SearchingMusicRepository { responseType: String.self ) } + + func deleteRecentMusicQueries(keyword: String) async { + await self.recentMusicQueriesPersistentStorage.deleteRecentQuery( + query: RecentMusicQueryDTO(query: keyword) + ) + } } diff --git a/StreetDrop/StreetDrop/Domain/Entity/Music.swift b/StreetDrop/StreetDrop/Domain/Entity/Music.swift index cabafca1..c36cd5f9 100644 --- a/StreetDrop/StreetDrop/Domain/Entity/Music.swift +++ b/StreetDrop/StreetDrop/Domain/Entity/Music.swift @@ -7,7 +7,7 @@ import Foundation -struct Music { +struct Music: Hashable { let albumName: String let artistName: String let songName: String diff --git a/StreetDrop/StreetDrop/Domain/Entity/RecommendMusic.swift b/StreetDrop/StreetDrop/Domain/Entity/RecommendMusic.swift index 76cc2201..ac272e28 100644 --- a/StreetDrop/StreetDrop/Domain/Entity/RecommendMusic.swift +++ b/StreetDrop/StreetDrop/Domain/Entity/RecommendMusic.swift @@ -16,3 +16,9 @@ struct RecommendMusicData: Decodable { let text: String let color: String } + +// TODO: jihye - api update +struct Artist: Hashable { + let name: String + let image: String +} diff --git a/StreetDrop/StreetDrop/Domain/UseCase/RecommendMusicUsecase.swift b/StreetDrop/StreetDrop/Domain/UseCase/RecommendMusicUsecase.swift new file mode 100644 index 00000000..2ef1adcb --- /dev/null +++ b/StreetDrop/StreetDrop/Domain/UseCase/RecommendMusicUsecase.swift @@ -0,0 +1,41 @@ +// +// RecommendMusicUsecase.swift +// StreetDrop +// +// Created by jihye kim on 07/08/2024. +// + +import Foundation + +import RxSwift + +protocol RecommendMusicUsecase { + func getPromptOfTheDay() -> Single + func getTrendingMusicList() -> Single<[Music]> + func getMostDroppedMusicList() -> Single<[Music]> + func getArtistList() -> Single<[Artist]> +} + +final class DefaultRecommendMusicUsecase: RecommendMusicUsecase { + private let recommendMusicRepository: RecommendMusicRepository + + init(recommendMusicRepository: RecommendMusicRepository = DefaultRecommendMusicRepository()) { + self.recommendMusicRepository = recommendMusicRepository + } + + func getPromptOfTheDay() -> Single { + recommendMusicRepository.fetchPromptOfTheDay() + } + + func getTrendingMusicList() -> Single<[Music]> { + recommendMusicRepository.fetchTrendingMusicList() + } + + func getMostDroppedMusicList() -> Single<[Music]> { + recommendMusicRepository.fetchMostDroppedMusicList() + } + + func getArtistList() -> Single<[Artist]> { + recommendMusicRepository.fetchArtistList() + } +} diff --git a/StreetDrop/StreetDrop/Domain/UseCase/SearchMusicUsecase.swift b/StreetDrop/StreetDrop/Domain/UseCase/SearchMusicUsecase.swift index e26265fe..ef40871e 100644 --- a/StreetDrop/StreetDrop/Domain/UseCase/SearchMusicUsecase.swift +++ b/StreetDrop/StreetDrop/Domain/UseCase/SearchMusicUsecase.swift @@ -15,6 +15,7 @@ protocol SearchMusicUsecase { func getRecentSearches() -> Single<[String]> func getVillageName(latitude: Double, longitude: Double) -> Single func fetchRecommendSearch() -> Single + func deleteRecentSearch(keyword: String) async } final class DefaultSearchingMusicUsecase: SearchMusicUsecase { @@ -44,4 +45,8 @@ final class DefaultSearchingMusicUsecase: SearchMusicUsecase { func fetchRecommendSearch() -> Single { return self.searchingMusicRepository.fetchRecommendMusicQueries() } + + func deleteRecentSearch(keyword: String) async { + await self.searchingMusicRepository.deleteRecentMusicQueries(keyword: keyword) + } } diff --git a/StreetDrop/StreetDrop/Presentation/EditView/View/EditCommentViewController.swift b/StreetDrop/StreetDrop/Presentation/EditView/View/EditCommentViewController.swift index d00d0703..1ce346c1 100644 --- a/StreetDrop/StreetDrop/Presentation/EditView/View/EditCommentViewController.swift +++ b/StreetDrop/StreetDrop/Presentation/EditView/View/EditCommentViewController.swift @@ -100,12 +100,10 @@ private extension EditCommentViewController { //MARK: - UI func configureUI() { - self.cancelButton.setTitle(Constant.empty, for: .normal) self.dropButton.setTitle(Constant.editButtonDisabledTitle, for: .disabled) self.dropButton.setTitle(Constant.editButtonNormalTitle, for: .normal) self.backButton.setTitle(Constant.empty, for: .normal) self.locationLabel.isHidden = true - self.cancelButton.setTitle(Constant.empty, for: .normal) self.topView.addSubview(titleLabel) titleLabel.snp.makeConstraints { diff --git a/StreetDrop/StreetDrop/Presentation/MusicDropView/View/MusicDropViewController.swift b/StreetDrop/StreetDrop/Presentation/MusicDropView/View/MusicDropViewController.swift index 40a004dc..acd9865b 100644 --- a/StreetDrop/StreetDrop/Presentation/MusicDropView/View/MusicDropViewController.swift +++ b/StreetDrop/StreetDrop/Presentation/MusicDropView/View/MusicDropViewController.swift @@ -71,15 +71,6 @@ class MusicDropViewController: UIViewController, Toastable, Alertable { return button }() - lazy var cancelButton: UIButton = { - let button: UIButton = UIButton() - button.setTitle("나가기", for: .normal) - button.setTitleColor(.gray300, for: .normal) - button.titleLabel?.font = .pretendard(size: 14, weight: 600) - - return button - }() - lazy var topView: UIView = UIView() private lazy var scrollView: UIScrollView = { @@ -299,16 +290,10 @@ private extension MusicDropViewController { }.disposed(by: disposeBag) backButton.rx.tap - .bind { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - .disposed(by: disposeBag) - - cancelButton.rx.tap .bind(with: self) { owner, _ in let dismissAction: AlertCompletion = { [weak self] in self?.navigationController?.dismiss(animated: true) - self?.navigationController?.popToRootViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } owner.showAlert( @@ -407,7 +392,7 @@ private extension MusicDropViewController { self.view.backgroundColor = .gray900 self.view.clipsToBounds = true - [backButton, cancelButton].forEach { + [backButton].forEach { topView.addSubview($0) } @@ -454,11 +439,6 @@ private extension MusicDropViewController { $0.trailing.equalTo(backButton.titleLabel!.snp.leading) } - cancelButton.snp.makeConstraints { - $0.trailing.equalToSuperview().inset(24) - $0.centerY.equalToSuperview() - } - scrollView.snp.makeConstraints { $0.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide) $0.top.equalTo(topView.snp.bottom) diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/GuideDetailView.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/GuideDetailView.swift index 9ff22293..1fe034c9 100644 --- a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/GuideDetailView.swift +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/GuideDetailView.swift @@ -8,8 +8,6 @@ import UIKit class GuideDetailView: UIView { - private let text: String - private lazy var speechBubblePointImageView: UIImageView = { let speechBubblePointImage = UIImage(named: "speechBubblePoint") let imageView = UIImageView(image: speechBubblePointImage) @@ -19,7 +17,6 @@ class GuideDetailView: UIView { private lazy var guideLabel: UILabel = { let label = UILabel() - label.text = self.text label.font = .pretendard(size: 12, weightName: .medium) label.numberOfLines = 0 label.textColor = .gray200 @@ -27,11 +24,14 @@ class GuideDetailView: UIView { return label }() - init(text: String) { - self.text = text + init() { super.init(frame: .zero) configureUI() } + + func configureText(_ text: String) { + guideLabel.text = text + } @available(*, unavailable) required init?(coder: NSCoder) { diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentQueryButton.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentQueryButton.swift deleted file mode 100644 index 50a598ad..00000000 --- a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentQueryButton.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// RecentQueryButton.swift -// StreetDrop -// -// Created by 차요셉 on 2023/05/25. -// - -import UIKit - -import RxRelay - -final class RecentQueryButton: UIControl { - var query: String { - get { - return self.queryLabel.text ?? "" - } - - set { - self.queryLabel.text = newValue - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - self.backgroundColor = UIColor(red: 0.225, green: 0.225, blue: 0.225, alpha: 1) - self.layer.cornerRadius = 16 - self.configureUI() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - UI - private lazy var queryLabel: UILabel = { - let label = UILabel() - label.font = .pretendard(size: 14, weight: 500) - label.setLineHeight(lineHeight: 16.71) - label.textColor = UIColor.gray150 - label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - return label - }() - - private lazy var deletingButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "deletingQuery"), for: .normal) - return button - }() - - private func configureUI() { - [ - self.queryLabel, - self.deletingButton - ].forEach { - self.addSubview($0) - } - - self.queryLabel.snp.makeConstraints { - $0.top.bottom.equalToSuperview().inset(8) - $0.leading.equalToSuperview().inset(12) - } - - self.deletingButton.snp.makeConstraints { - $0.width.height.equalTo(8) - $0.top.bottom.equalToSuperview().inset(12.5) - $0.leading.equalTo(self.queryLabel.snp.trailing).offset(6) - $0.trailing.equalToSuperview().inset(12) - } - } -} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentQueryCell.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentQueryCell.swift new file mode 100644 index 00000000..2465c494 --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentQueryCell.swift @@ -0,0 +1,95 @@ +// +// RecentQueryCell.swift +// StreetDrop +// +// Created by 차요셉 on 2023/05/25. +// + +import UIKit + +import RxRelay +import RxSwift + +final class RecentQueryCell: UICollectionViewCell { + var query: String { + self.queryLabel.text ?? "" + } + + private var deletingButtonTappedEvent: PublishRelay? + private let disposeBag = DisposeBag() + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .gray600 + self.layer.cornerRadius = frame.height / 2 + self.configureUI() + self.bindAction() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI + private lazy var queryLabel: UILabel = { + let label = UILabel() + label.font = .pretendard(size: 14, weight: 500) + label.setLineHeight(lineHeight: 16.71) + label.textColor = UIColor.gray150 + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return label + }() + + private lazy var deletingButton: UIButton = { + let button = UIButton() + button.setImage( + // TODO: jihye - figma image update + UIImage(named: "deletingQuery")?.withRenderingMode(.alwaysTemplate), + for: .normal + ) + button.tintColor = .gray400 + return button + }() + + private func configureUI() { + [ + self.queryLabel, + self.deletingButton + ].forEach { + self.addSubview($0) + } + + self.queryLabel.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(6) + $0.leading.equalToSuperview().inset(10) + } + + self.deletingButton.snp.makeConstraints { + $0.width.height.equalTo(16) + $0.centerY.equalTo(self.queryLabel) + $0.leading.equalTo(self.queryLabel.snp.trailing).offset(2) + $0.trailing.equalToSuperview().inset(6) + } + } + + private func bindAction() { + deletingButton.rx.tap + .bind { [weak self] in + guard let self, + let query = self.queryLabel.text + else { return } + + self.deletingButtonTappedEvent?.accept(query) + } + .disposed(by: disposeBag) + } + + func configure( + with query: String, + deletingButtonTappedEvent: PublishRelay + ) { + self.queryLabel.text = query + self.deletingButtonTappedEvent = deletingButtonTappedEvent + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentSearchesHeaderView.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentSearchesHeaderView.swift new file mode 100644 index 00000000..b6fb27b3 --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecentSearchesHeaderView.swift @@ -0,0 +1,43 @@ +// +// RecentSearchesHeaderView.swift +// StreetDrop +// +// Created by jihye kim on 12/08/2024. +// + +import UIKit + +import SnapKit + +class RecentSearchesHeaderView: UICollectionReusableView { + static var reuseIdentifier: String { + return String(describing: Self.self) + } + + let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + titleLabel.font = .pretendard(size: 14, weightName: .medium) + titleLabel.textColor = .gray150 + + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + } + + func configure(with title: String) { + titleLabel.text = title + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendArtistCell.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendArtistCell.swift new file mode 100644 index 00000000..2768717b --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendArtistCell.swift @@ -0,0 +1,77 @@ +// +// RecommendArtistCell.swift +// StreetDrop +// +// Created by jihye kim on 07/08/2024. +// + +import UIKit + +import Kingfisher + +class RecommendArtistCell: UICollectionViewCell { + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been impl") + } + + private lazy var imageView: UIImageView = { + let view: UIImageView = UIImageView() + view.backgroundColor = .gray700 + view.layer.cornerRadius = 15 + view.clipsToBounds = true + + return view + }() + + private lazy var nameLabel: UILabel = { + let label: UILabel = UILabel() + label.font = .pretendard(size: 12, weightName: .semiBold) + label.setLineHeight(lineHeight: 16) + label.textColor = .white + label.numberOfLines = 1 + + return label + }() + + func configure(with item: Artist) { + nameLabel.text = item.name + + if let artistImageUrl = URL(string: item.image) { + imageView.kf.setImage(with: artistImageUrl) + } + } +} + +private extension RecommendArtistCell { + func configureUI() { + self.contentView.backgroundColor = .gray600 + self.contentView.layer.cornerRadius = self.contentView.frame.height / 2 + self.contentView.layer.masksToBounds = true + + [ + self.imageView, + self.nameLabel + ].forEach { + self.contentView.addSubview($0) + } + + self.imageView.snp.makeConstraints { + $0.width.height.equalTo(30) + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().offset(5) + $0.verticalEdges.equalToSuperview().inset(5) + } + + self.nameLabel.snp.makeConstraints { + $0.verticalEdges.equalToSuperview().inset(10) + $0.leading.equalTo(self.imageView.snp.trailing).offset(8) + $0.trailing.equalToSuperview().inset(15) + } + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendHeaderView.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendHeaderView.swift new file mode 100644 index 00000000..7d80002d --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendHeaderView.swift @@ -0,0 +1,125 @@ +// +// RecommendHeaderView.swift +// StreetDrop +// +// Created by jihye kim on 09/08/2024. +// + +import UIKit + +import RxRelay +import RxSwift +import SnapKit + +class RecommendHeaderView: UICollectionReusableView { + static var reuseIdentifier: String { + return String(describing: Self.self) + } + + let titleLabel = UILabel() + let arrowIconImageView = UIImageView() + let infoIconButton = UIButton() + private let disposeBag: DisposeBag = DisposeBag() + private let infoGuideView = GuideDetailView() + + private var didTapAction: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + let tapAreaView = UIView() + let tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(didTap) + ) + tapAreaView.addGestureRecognizer(tapGesture) + + addSubview(tapAreaView) + tapAreaView.snp.makeConstraints { make in + make.leading.top.bottom.equalToSuperview() + } + + titleLabel.font = .pretendard(size: 20, weightName: .bold) + titleLabel.textColor = .white + + tapAreaView.addSubview(titleLabel) + + titleLabel.snp.makeConstraints { make in + make.leading.top.bottom.equalToSuperview() + } + + arrowIconImageView.image = UIImage(named: "goTo") + tapAreaView.addSubview(arrowIconImageView) + + arrowIconImageView.snp.makeConstraints { make in + make.width.height.equalTo(24) + make.leading.equalTo(titleLabel.snp.trailing) + make.centerY.equalToSuperview() + make.trailing.equalToSuperview() + } + + infoIconButton.setImage( + UIImage(named: "infoIcon")? + .resized(to: CGSize(width: 20, height: 20))? + .withRenderingMode(.alwaysTemplate), + for: .normal + ) + infoIconButton.addAction( + UIAction { [weak self] _ in + guard let self else { return } + self.infoIconButton.isSelected.toggle() + let isSelected = self.infoIconButton.isSelected + self.infoIconButton.tintColor = isSelected ? .primary400 : .gray200 + + UIView.animate(withDuration: 0.3) { + self.infoGuideView.alpha = isSelected ? 1 : 0 + self.infoGuideView.isUserInteractionEnabled = isSelected + } + }, + for: .touchUpInside + ) + infoIconButton.tintColor = .gray200 + addSubview(infoIconButton) + + infoIconButton.snp.makeConstraints { make in + make.width.height.equalTo(20) + make.centerY.equalToSuperview() + make.trailing.equalToSuperview() + } + + addSubview(infoGuideView) + infoGuideView.snp.makeConstraints { make in + make.trailing.equalTo(infoIconButton).offset(7) + make.top.equalTo(infoIconButton.snp.bottom).offset(16) + } + } + + @objc private func didTap() { + self.didTapAction?() + } + + func configure( + with title: String, + hideArrow: Bool = false, + infoText: String? = nil, + didTapAction: (() -> Void)? = nil + ) { + self.titleLabel.text = title + self.arrowIconImageView.isHidden = hideArrow + self.didTapAction = didTapAction + + if let infoText { + self.infoIconButton.isHidden = false + infoGuideView.configureText(infoText) + } else { + self.infoIconButton.isHidden = true + } + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendMusicCell.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendMusicCell.swift new file mode 100644 index 00000000..f27ac3b9 --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendMusicCell.swift @@ -0,0 +1,93 @@ +// +// RecommendMusicCell.swift +// StreetDrop +// +// Created by jihye kim on 07/08/2024. +// + +import UIKit + +import Kingfisher + +class RecommendMusicCell: UICollectionViewCell { + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been impl") + } + + private lazy var albumCoverImageView: UIImageView = { + let view: UIImageView = UIImageView() + view.backgroundColor = .gray700 + view.layer.cornerRadius = 8 + view.clipsToBounds = true + + return view + }() + + private lazy var titleLabel: UILabel = { + let label: UILabel = UILabel() + label.font = .pretendard(size: 14, weightName: .medium) + label.setLineHeight(lineHeight: 20) + label.textColor = .white + label.numberOfLines = 2 + + return label + }() + + private lazy var artistLabel: UILabel = { + let label: UILabel = UILabel() + label.font = .pretendard(size: 12, weightName: .regular) + label.setLineHeight(lineHeight: 16) + label.textColor = .gray200 + label.numberOfLines = 1 + + return label + }() + + func configure(with item: Music) { + titleLabel.text = item.songName + artistLabel.text = item.artistName + + if let albumImageUrl = URL(string: item.albumThumbnailImage) { + albumCoverImageView.kf.setImage(with: albumImageUrl) + } + } +} + +private extension RecommendMusicCell { + func configureUI() { + let infoStackView = UIStackView() + infoStackView.axis = .vertical + + [ + self.albumCoverImageView, + infoStackView + ].forEach { + self.contentView.addSubview($0) + } + + [ + self.titleLabel, + self.artistLabel + ].forEach { + infoStackView.addArrangedSubview($0) + } + + self.albumCoverImageView.snp.makeConstraints { + $0.top.leading.bottom.equalToSuperview() + $0.width.height.equalTo(48) + } + + infoStackView.snp.makeConstraints { + $0.leading.equalTo(albumCoverImageView.snp.trailing).offset(8) + $0.top.greaterThanOrEqualToSuperview() + $0.bottom.lessThanOrEqualToSuperview() + $0.centerY.trailing.equalToSuperview() + } + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendMusicListViewController.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendMusicListViewController.swift new file mode 100644 index 00000000..38153ec8 --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/RecommendMusicListViewController.swift @@ -0,0 +1,175 @@ +// +// RecommendMusicListViewController.swift +// StreetDrop +// +// Created by jihye kim on 15/08/2024. +// + +import UIKit + +import RxSwift +import SnapKit + +final class RecommendMusicListViewController: UIViewController { + private let viewModel: DefaultRecommendMusicListViewModel + private let disposeBag = DisposeBag() + + private var dataSource: UITableViewDiffableDataSource? + + private lazy var navigationBar: UIView = { + let view = UIView() + return view + }() + + private lazy var backButton: UIButton = { + let button: UIButton = UIButton() + button.setImage(UIImage(named: "backButton"), for: .normal) + return button + }() + + private lazy var navigationTitleLabel: UILabel = { + let label: UILabel = UILabel() + label.text = self.viewModel.title + label.textColor = .gray100 + label.font = .pretendard(size: 16, weightName: .bold) + label.setLineHeight(lineHeight: 24) + return label + }() + + private lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .black + tableView.register( + SearchingMusicTableViewCell.self, + forCellReuseIdentifier: SearchingMusicTableViewCell.identifier + ) + tableView.contentInset = UIEdgeInsets( + top: 16, left: 0, bottom: 32, right: 0 + ) + tableView.rowHeight = 76 + tableView.keyboardDismissMode = .onDrag + + return tableView + }() + + init( + viewModel: DefaultRecommendMusicListViewModel + ) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + bindAction() + bindViewModel() + configureUI() + configureDataSource() + } + + private func configureUI() { + self.view.backgroundColor = .black + + [ + self.backButton, + self.navigationTitleLabel + ].forEach { + self.navigationBar.addSubview($0) + } + + [ + self.navigationBar, + self.tableView + ].forEach { + self.view.addSubview($0) + } + + self.navigationBar.snp.makeConstraints { + $0.height.equalTo(60) + $0.top.equalTo(self.view.safeAreaLayoutGuide) + $0.leading.equalToSuperview() + $0.trailing.equalToSuperview() + } + + self.backButton.snp.makeConstraints { + $0.width.height.equalTo(32) + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().inset(16) + } + + self.navigationTitleLabel.snp.makeConstraints { + $0.centerX.centerY.equalToSuperview() + } + + self.tableView.snp.makeConstraints { + $0.top.equalTo(self.navigationBar.snp.bottom) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(self.view.safeAreaLayoutGuide) + } + } + + private func bindAction() { + Observable.merge( + self.backButton.rx.tap.asObservable() + ) + .bind { _ in + self.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + } + + private func bindViewModel() { + let musicDidPressEvent = self.tableView.rx.itemSelected.map { $0.row } + + let input = DefaultRecommendMusicListViewModel.Input( + musicDidPressEvent: musicDidPressEvent + ) + let output = viewModel.convert(input: input, disposedBag: disposeBag) + + output.selectedMusic + .bind { [weak self] music in + guard let self else { return } + let musicDropViewController = MusicDropViewController( + viewModel: MusicDropViewModel( + droppingInfo: DroppingInfo( + location: .init( + latitude: self.viewModel.location.coordinate.latitude, + longitude: self.viewModel.location.coordinate.longitude, + address: self.viewModel.address + ), + music: music + ) + ) + ) + + self.navigationController?.pushViewController( + musicDropViewController, + animated: true + ) + } + .disposed(by: disposeBag) + } + + private func configureDataSource() { + dataSource = UITableViewDiffableDataSource( + tableView: tableView + ) { tableView, indexPath, music in + let cell = tableView.dequeueReusableCell( + withIdentifier: SearchingMusicTableViewCell.identifier, + for: indexPath + ) as? SearchingMusicTableViewCell + cell?.setData(music: music) + return cell + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([0]) + snapshot.appendItems(self.viewModel.musicList) + dataSource?.apply(snapshot, animatingDifferences: true) + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/SearchingMusicViewController.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/SearchingMusicViewController.swift index 2f12c691..019d6c75 100644 --- a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/SearchingMusicViewController.swift +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/View/SearchingMusicViewController.swift @@ -12,9 +12,63 @@ import RxCocoa import SnapKit import GoogleMobileAds +struct RecommendMusicSectionModel { + let type: SectionType + let items: [Item] + + enum SectionType: Hashable { + case recentSearchKeyword + case trendingMusic + case mostDroppedMusic + case artist + + var title: String { + switch self { + case .recentSearchKeyword: + return "최근 검색어" + case .trendingMusic: + return "지금 인기 있는 음악" + case .mostDroppedMusic: + return "많이 드랍된 음악" + case .artist: + return "아티스트" + } + } + + var infoText: String? { + switch self { + case .trendingMusic: + return "애플 뮤직의 ‘지금 인기 있는 곡’\n리스트를 반영했어요." + default: + return nil + } + } + } + + enum Item: Hashable { + case recentSearchKeyword(String) + case trendingMusic(Music) + case mostDroppedMusic(Music) + case artist(Artist) + } +} + final class SearchingMusicViewController: UIViewController { + typealias Section = RecommendMusicSectionModel.SectionType + typealias Item = RecommendMusicSectionModel.Item + typealias DataSource = UICollectionViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + private let viewModel: DefaultSearchingMusicViewModel private let disposeBag: DisposeBag = DisposeBag() + private let viewDidLoadEvent = PublishRelay() + private let deletingButtonTappedEvent = PublishRelay() + private let recentQueryDidPressEvent = PublishRelay() + private let artistQueryDidPressEvent = PublishRelay() + private let musicDidPressEvent = PublishRelay() + + private var collectionView: UICollectionView? + private var dataSource: DataSource? init(viewModel: DefaultSearchingMusicViewModel) { self.viewModel = viewModel @@ -32,7 +86,11 @@ final class SearchingMusicViewController: UIViewController { bindAction() bindViewModel() configureUI() + configureCollectionView() + configureDataSource() configureGADBannerView() + + self.viewDidLoadEvent.accept(Void()) } // MARK: - UI @@ -44,6 +102,7 @@ final class SearchingMusicViewController: UIViewController { private lazy var searchTextField: UITextField = { let textField: UITextField = UITextField() + textField.font = .pretendard(size: 14, weightName: .medium) textField.backgroundColor = UIColor.gray700 textField.attributedPlaceholder = NSAttributedString( string: "드랍할 음악 검색", @@ -89,37 +148,7 @@ final class SearchingMusicViewController: UIViewController { return button }() - private lazy var recentMusicSearchView: UIView = { - let view: UIView = UIView() - return view - }() - - private lazy var recentSearchResultLabel: UILabel = { - let label: UILabel = UILabel() - label.text = "최근 검색어" - label.textColor = UIColor.gray150 - label.font = .pretendard(size: 14, weight: 500) - label.setLineHeight(lineHeight: 20) - return label - }() - - private lazy var recentMusicSearchScrollView: RecentMusicSearchScrollView = { - let scrollView = RecentMusicSearchScrollView() - return scrollView - }() - - private lazy var recommendMusicSearchCollectionView: RecommendMusicSearchCollectionView = { - let collectionView = RecommendMusicSearchCollectionView() - return collectionView - }() - - private lazy var questionLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - label.font = UIFont(name: "Pretendard-Bold", size: 20) - return label - }() - + // TODO: jihye -> RecommendMusicListViewController private lazy var tableView: UITableView = { let tableView = UITableView() tableView.backgroundColor = .clear @@ -127,9 +156,11 @@ final class SearchingMusicViewController: UIViewController { SearchingMusicTableViewCell.self, forCellReuseIdentifier: SearchingMusicTableViewCell.identifier ) + tableView.contentInset = UIEdgeInsets( + top: 8, left: 0, bottom: 32, right: 0 + ) tableView.rowHeight = 76 tableView.keyboardDismissMode = .onDrag - return tableView }() @@ -139,6 +170,377 @@ final class SearchingMusicViewController: UIViewController { }() } +extension SearchingMusicViewController { + private func displayList( + queryItems: [Item], + trendingItems: [Item], + droppedItems: [Item], + artistItems: [Item] + ) { + var snapshot = Snapshot() + + if !queryItems.isEmpty { + snapshot.appendSections([Section.recentSearchKeyword]) + snapshot.appendItems(queryItems, toSection: .recentSearchKeyword) + } + + if !trendingItems.isEmpty { + snapshot.appendSections([Section.trendingMusic]) + snapshot.appendItems(trendingItems, toSection: .trendingMusic) + } + + if !droppedItems.isEmpty { + snapshot.appendSections([Section.mostDroppedMusic]) + snapshot.appendItems(droppedItems, toSection: .mostDroppedMusic) + } + + if !artistItems.isEmpty { + snapshot.appendSections([Section.artist]) + snapshot.appendItems(artistItems, toSection: .artist) + } + + dataSource?.apply(snapshot, animatingDifferences: true) + } +} + +// MARK: CollectionView + +extension SearchingMusicViewController: UICollectionViewDelegate { + private func configureCollectionView() { + let collectionView = UICollectionView( + frame: view.bounds, + collectionViewLayout: createLayout() + ) + collectionView.backgroundColor = .clear + collectionView.delegate = self + collectionView.contentInset = UIEdgeInsets( + top: 16, left: 0, bottom: 0, right: 0 + ) + view.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.top.equalTo(self.searchView.snp.bottom) + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(self.bannerView.snp.top) + } + + self.collectionView = collectionView + } + + private func configureDataSource() { + typealias CellRegistration = UICollectionView.CellRegistration + typealias RecentSearchCellRegistration = CellRegistration + typealias MusicCellRegistration = CellRegistration + typealias ArtistCellRegistration = CellRegistration + + guard let collectionView else { return } + + let recentSearchCellRegistration = RecentSearchCellRegistration { [weak self] cell, indexPath, item in + guard let self else { return } + cell.configure(with: item, deletingButtonTappedEvent: self.deletingButtonTappedEvent) + } + + let musicCellRegistration = MusicCellRegistration { cell, indexPath, item in + cell.configure(with: item) + } + + let artistCellRegistration = ArtistCellRegistration { cell, indexPath, item in + cell.configure(with: item) + } + + collectionView.register( + RecentSearchesHeaderView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: RecentSearchesHeaderView.reuseIdentifier + ) + + collectionView.register( + RecommendHeaderView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: RecommendHeaderView.reuseIdentifier + ) + + dataSource = DataSource( + collectionView: collectionView + ) { collectionView, indexPath, item -> UICollectionViewCell? in + + switch item { + case .recentSearchKeyword(let keyword): + return collectionView.dequeueConfiguredReusableCell( + using: recentSearchCellRegistration, + for: indexPath, + item: keyword + ) + + case .trendingMusic(let music): + return collectionView.dequeueConfiguredReusableCell( + using: musicCellRegistration, + for: indexPath, + item: music + ) + + case .mostDroppedMusic(let music): + return collectionView.dequeueConfiguredReusableCell( + using: musicCellRegistration, + for: indexPath, + item: music + ) + + case .artist(let artist): + return collectionView.dequeueConfiguredReusableCell( + using: artistCellRegistration, + for: indexPath, + item: artist + ) + } + } + + dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in + guard let dataSource = self?.dataSource, + let section = dataSource.snapshot().sectionIdentifiers[safe: indexPath.section] + else { return nil } + + switch section { + case .recentSearchKeyword: + let headerView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: RecentSearchesHeaderView.reuseIdentifier, + for: indexPath + ) as? RecentSearchesHeaderView + headerView?.configure(with: section.title) + + return headerView + + case .trendingMusic: + let headerView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: RecommendHeaderView.reuseIdentifier, + for: indexPath + ) as? RecommendHeaderView + headerView?.configure( + with: section.title, + infoText: section.infoText + ) { [weak self] in + guard let self else { return } + self.routeToMusicList( + title: section.title, + musicList: self.viewModel.trendingMusicList + ) + } + + return headerView + + case .mostDroppedMusic: + let headerView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: RecommendHeaderView.reuseIdentifier, + for: indexPath + ) as? RecommendHeaderView + headerView?.configure(with: section.title) { [weak self] in + guard let self else { return } + self.routeToMusicList( + title: section.title, + musicList: self.viewModel.mostDroppedMusicList + ) + } + + return headerView + + case .artist: + let headerView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: RecommendHeaderView.reuseIdentifier, + for: indexPath + ) as? RecommendHeaderView + headerView?.configure(with: section.title, hideArrow: true) + + return headerView + } + } + } + + private func routeToMusicList(title: String, musicList: [Music]) { + let viewController = RecommendMusicListViewController( + viewModel: DefaultRecommendMusicListViewModel( + title: title, + musicList: musicList, + location: self.viewModel.location, + address: self.viewModel.address + ) + ) + + self.navigationController?.pushViewController( + viewController, + animated: true + ) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let item = self.dataSource?.itemIdentifier(for: indexPath) else { return } + + switch item { + case .recentSearchKeyword(let keyword): + self.showSearchResultList(with: keyword) + self.recentQueryDidPressEvent.accept(keyword) + case .trendingMusic(let music): + self.musicDidPressEvent.accept(music) + case .mostDroppedMusic(let music): + self.musicDidPressEvent.accept(music) + case .artist(let artist): + self.showSearchResultList(with: artist.name) + self.artistQueryDidPressEvent.accept(artist.name) + } + } + + private func showSearchResultList(with query: String) { + self.searchTextField.text = query + self.tableView.isHidden = false + self.collectionView?.isHidden = true + self.searchTextField.resignFirstResponder() + } +} + +// MARK: - CollectionView Layout + +extension SearchingMusicViewController { + private func createLayout() -> UICollectionViewLayout { + UICollectionViewCompositionalLayout { [weak self] sectionIndex, layoutEnvironment in + guard let self, + let dataSource = self.dataSource, + let section = dataSource.snapshot().sectionIdentifiers[safe: sectionIndex] + else { return nil } + + // TODO: jihye - bottom inset update + if section == .recentSearchKeyword { + return self.createRecentSearchKeywordSectionLayout() + } else if section == .artist { + return self.createArtistSectionLayout() + } else { + return self.createMusicListSectionLayout() + } + } + } + + private func createRecentSearchKeywordSectionLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .estimated(105), + heightDimension: .absolute(28) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .estimated(105), + heightDimension: .absolute(28) + ) + + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.interGroupSpacing = 8 + section.contentInsets = NSDirectionalEdgeInsets( + top: 8, leading: 24, bottom: 48, trailing: 24 + ) + + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(20) + ) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + + section.boundarySupplementaryItems = [sectionHeader] + + return section + } + + private func createArtistSectionLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .estimated(50), + heightDimension: .absolute(40) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(40) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + group.interItemSpacing = .fixed(12) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 12 + section.contentInsets = NSDirectionalEdgeInsets( + top: 20, leading: 24, bottom: 32, trailing: 24 + ) + section.orthogonalScrollingBehavior = .none + + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(28) + ) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + + section.boundarySupplementaryItems = [sectionHeader] + + return section + } + + // trending music, mostDropped music + private func createMusicListSectionLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.7), + heightDimension: .estimated(176) + ) + + let group = NSCollectionLayoutGroup.vertical( + layoutSize: groupSize, subitem: item, count: 3 + ) + + group.interItemSpacing = .fixed(16) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.interGroupSpacing = 16 + section.contentInsets = NSDirectionalEdgeInsets( + top: 20, leading: 24, bottom: 48, trailing: 24 + ) + + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(28) + ) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + + section.boundarySupplementaryItems = [sectionHeader] + + return section + } +} + private extension SearchingMusicViewController { // MARK: - UI @@ -154,29 +556,11 @@ private extension SearchingMusicViewController { .bind { keyword in if let keyword = keyword { self.tableView.isHidden = keyword.isEmpty - self.recentMusicSearchView.isHidden = !keyword.isEmpty + self.collectionView?.isHidden = !keyword.isEmpty } } .disposed(by: disposeBag) - self.recentMusicSearchScrollView.queryButtonDidTappedEvent - .bind { recentQuery in - self.searchTextField.text = recentQuery - self.tableView.isHidden = false - self.recentMusicSearchView.isHidden = true - self.searchTextField.resignFirstResponder() - } - .disposed(by: disposeBag) - - self.recommendMusicSearchCollectionView.queryButtonDidTappedEvent - .bind { recentQuery in - self.searchTextField.text = recentQuery - self.tableView.isHidden = false - self.recentMusicSearchView.isHidden = true - self.searchTextField.resignFirstResponder() - } - .disposed(by: disposeBag) - self.backButton.rx.tap .bind { self.navigationController?.popViewController(animated: true) @@ -190,17 +574,19 @@ private extension SearchingMusicViewController { let selectedTableViewCellEvent = self.tableView.rx.itemSelected.map { $0.row } - let searchTextFieldEmptyEvent = self.searchTextField.rx.text.orEmpty.filter{ + let searchTextFieldEmptyEvent = self.searchTextField.rx.text.orEmpty.filter { $0.isEmpty }.map { _ in } let input = DefaultSearchingMusicViewModel.Input( - viewDidAppearEvent: .just(()), + viewDidLoadEvent: self.viewDidLoadEvent, searchTextFieldEmptyEvent: searchTextFieldEmptyEvent, keyBoardDidPressSearchEventWithKeyword: keyBoardDidPressSearchEventWithKeyword, - recentQueryDidPressEvent: self.recentMusicSearchScrollView.queryButtonDidTappedEvent, - recommendQueryDidPressEvent: self.recommendMusicSearchCollectionView.queryButtonDidTappedEvent, - tableViewCellDidPressedEvent: selectedTableViewCellEvent + recentQueryDidPressEvent: self.recentQueryDidPressEvent, + artistQueryDidPressEvent: self.artistQueryDidPressEvent, + musicDidPressEvent: self.musicDidPressEvent, + tableViewCellDidPressedEvent: selectedTableViewCellEvent, + deletingButtonTappedEvent: self.deletingButtonTappedEvent ) let output = viewModel.convert(input: input, disposedBag: disposeBag) @@ -215,20 +601,27 @@ private extension SearchingMusicViewController { } .disposed(by: disposeBag) - output.recentMusicQueries - .observe(on: MainScheduler.instance) - .bind { queries in - self.recentMusicSearchScrollView.setData(queries: queries) - } - .disposed(by: disposeBag) - - output.recommendMusicQueries - .observe(on: MainScheduler.instance) - .bind { queries in - self.setRecommendData(queries.description) - self.recommendMusicSearchCollectionView.setData(queries: queries.terms) - } - .disposed(by: disposeBag) + Observable.combineLatest( + output.recentMusicQueries, + output.trendingMusicList, + output.mostDroppedMusicList, + output.artists + ) + .observe(on: MainScheduler.instance) + .bind { [weak self] recentQueries, trendingList, droppedList, artists in + let queryItems = recentQueries.map { Item.recentSearchKeyword($0) } + let trendingItems = trendingList.map { Item.trendingMusic($0) } + let droppedItems = droppedList.map { Item.mostDroppedMusic($0) } + let artistItems = artists.map { Item.artist($0) } + + self?.displayList( + queryItems: queryItems, + trendingItems: trendingItems, + droppedItems: droppedItems, + artistItems: artistItems + ) + } + .disposed(by: disposeBag) output.selectedMusic .bind { [weak self] music in @@ -262,21 +655,11 @@ private extension SearchingMusicViewController { self.searchView.addSubview($0) } - [ - self.recentSearchResultLabel, - self.recentMusicSearchScrollView, - self.questionLabel, - self.recommendMusicSearchCollectionView - ].forEach { - self.recentMusicSearchView.addSubview($0) - } - self.searchCancelView.addSubview(self.searchCancelButton) [ self.searchView, self.tableView, - self.recentMusicSearchView, self.bannerView ].forEach { self.view.addSubview($0) @@ -315,37 +698,10 @@ private extension SearchingMusicViewController { $0.leading.equalToSuperview() } - self.recentMusicSearchView.snp.makeConstraints { - $0.top.equalTo(self.searchView.snp.bottom).offset(22) - $0.leading.trailing.bottom.equalToSuperview() - } - - self.recentSearchResultLabel.snp.makeConstraints { - $0.top.equalToSuperview() - $0.leading.equalToSuperview().inset(24) - } - - self.recentMusicSearchScrollView.snp.makeConstraints { - $0.top.equalTo(self.recentSearchResultLabel.snp.bottom).offset(8) - $0.leading.trailing.equalToSuperview().inset(24) - $0.height.equalTo(33) - } - - self.questionLabel.snp.makeConstraints { - $0.top.equalTo(self.recentMusicSearchScrollView.snp.bottom).offset(54) - $0.leading.trailing.equalToSuperview().inset(24) - } - - self.recommendMusicSearchCollectionView.snp.makeConstraints { - $0.top.equalTo(self.questionLabel.snp.bottom).offset(24) - $0.leading.trailing.equalToSuperview().inset(24) - $0.height.equalTo(144) - } - self.tableView.snp.makeConstraints { - $0.top.equalTo(self.searchView.snp.bottom).offset(26) + $0.top.equalTo(self.searchView.snp.bottom).offset(8) $0.leading.trailing.equalToSuperview() - $0.bottom.equalToSuperview().inset(60) + $0.bottom.equalTo(bannerView.snp.top) } self.bannerView.snp.makeConstraints { @@ -356,22 +712,6 @@ private extension SearchingMusicViewController { } } - func setRecommendData(_ query: [RecommendMusicData]) { - let attributedString = NSMutableAttributedString() - let style = NSMutableParagraphStyle() - style.maximumLineHeight = 32 - style.minimumLineHeight = 32 - - for data in query { - let attributes: [NSAttributedString.Key : Any] = [ - .foregroundColor: FontType(rawValue: data.color)?.foregroundColor ?? .white, - .paragraphStyle: style - ] - attributedString.append(NSAttributedString(string: data.text, attributes: attributes)) - } - questionLabel.attributedText = attributedString - } - func configureGADBannerView() { bannerView.adUnitID = GADUnitID.searchMusic bannerView.rootViewController = self @@ -387,6 +727,15 @@ extension SearchingMusicViewController: UITextFieldDelegate { textField.resignFirstResponder() return true } + + private func configureSearchBarPlaceholder(with prompt: String) { + searchTextField.attributedPlaceholder = NSAttributedString( + string: prompt, + attributes: [ + .foregroundColor: UIColor.gray300 + ] + ) + } } // MARK: - GADBannerViewDelegate diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/RecommendMusicListViewModel.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/RecommendMusicListViewModel.swift new file mode 100644 index 00000000..199660ee --- /dev/null +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/RecommendMusicListViewModel.swift @@ -0,0 +1,62 @@ +// +// DefaultRecommendMusicListViewModel.swift +// StreetDrop +// +// Created by jihye kim on 15/08/2024. +// + +import CoreLocation +import Foundation + +import RxRelay +import RxSwift + +protocol RecommendMusicListViewModel: ViewModel { } + +final class DefaultRecommendMusicListViewModel: RecommendMusicListViewModel { + private let model: RecommendMusicUsecase + private let disposeBag: DisposeBag = DisposeBag() + + let title: String + let musicList: [Music] + let location: CLLocation + let address: String + + struct Input { + let musicDidPressEvent: Observable + } + + struct Output { + let musicList = PublishRelay<[Music]>() + let selectedMusic = PublishRelay() + } + + init( + model: RecommendMusicUsecase = DefaultRecommendMusicUsecase(), + title: String, + musicList: [Music], + location: CLLocation, + address: String + ) { + self.model = model + self.title = title + self.musicList = musicList + self.location = location + self.address = address + } + + func convert(input: Input, disposedBag: DisposeBag) -> Output { + let output = Output() + + input.musicDidPressEvent + .bind { [weak self] indexPathRow in + guard let self, + let music = self.musicList[safe: indexPathRow] + else { return } + output.selectedMusic.accept(music) + } + .disposed(by: disposedBag) + + return output + } +} diff --git a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/SearchingMusicViewModel.swift b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/SearchingMusicViewModel.swift index 76de87be..1e1e5f86 100644 --- a/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/SearchingMusicViewModel.swift +++ b/StreetDrop/StreetDrop/Presentation/SearchingMusicScene/ViewModel/SearchingMusicViewModel.swift @@ -13,44 +13,58 @@ import RxRelay import RxSwift protocol SearchingMusicViewModel: ViewModel { + var trendingMusicList: [Music] { get } + var mostDroppedMusicList: [Music] { get } + func searchMusic(output: Output, keyword: String) } final class DefaultSearchingMusicViewModel: SearchingMusicViewModel { private let model: SearchMusicUsecase + private let recommendMusicUseCase: RecommendMusicUsecase let location: CLLocation var address: String = "" private let disposeBag: DisposeBag = DisposeBag() private var musicList: [Music] = [] + var trendingMusicList: [Music] = [] + var mostDroppedMusicList: [Music] = [] + struct Input { - let viewDidAppearEvent: Observable + let viewDidLoadEvent: PublishRelay let searchTextFieldEmptyEvent: Observable let keyBoardDidPressSearchEventWithKeyword: Observable let recentQueryDidPressEvent: PublishRelay - let recommendQueryDidPressEvent: PublishRelay + let artistQueryDidPressEvent: PublishRelay + let musicDidPressEvent: PublishRelay let tableViewCellDidPressedEvent: Observable + let deletingButtonTappedEvent: PublishRelay } - + struct Output { let searchedMusicList = PublishRelay<[Music]>() - let recentMusicQueries = BehaviorRelay<[String]>(value: [""]) + let recentMusicQueries = PublishRelay<[String]>() let selectedMusic = PublishRelay() - let recommendMusicQueries = PublishRelay() + let promptOfTheDay = PublishRelay() + let trendingMusicList = PublishRelay<[Music]>() + let mostDroppedMusicList = PublishRelay<[Music]>() + let artists = PublishRelay<[Artist]>() } init( model: SearchMusicUsecase = DefaultSearchingMusicUsecase(), + recommendMusicUseCase: RecommendMusicUsecase = DefaultRecommendMusicUsecase(), location: CLLocation ) { self.model = model + self.recommendMusicUseCase = recommendMusicUseCase self.location = location } func convert(input: Input, disposedBag: DisposeBag) -> Output { let output = Output() - input.viewDidAppearEvent + input.viewDidLoadEvent .subscribe(onNext: { [weak self] in self?.model.getRecentSearches() .subscribe { result in @@ -62,15 +76,49 @@ final class DefaultSearchingMusicViewModel: SearchingMusicViewModel { } } .disposed(by: disposedBag) - self?.model.fetchRecommendSearch().subscribe { result in - switch result { - case .success(let queries): - output.recommendMusicQueries.accept(queries) - case .failure(_): - print("failure") - } - }.disposed(by: disposedBag) self?.fetchCurrentLocationVillageName() + self?.recommendMusicUseCase.getPromptOfTheDay() + .subscribe { result in + switch result { + case .success(let prompt): + output.promptOfTheDay.accept(prompt) + case .failure(_): + output.mostDroppedMusicList.accept([]) + } + } + .disposed(by: disposedBag) + self?.recommendMusicUseCase.getTrendingMusicList() + .subscribe { [weak self] result in + switch result { + case .success(let musicList): + output.trendingMusicList.accept(musicList) + self?.trendingMusicList = musicList + case .failure(_): + output.mostDroppedMusicList.accept([]) + } + } + .disposed(by: disposedBag) + self?.recommendMusicUseCase.getMostDroppedMusicList() + .subscribe { result in + switch result { + case .success(let musicList): + output.mostDroppedMusicList.accept(musicList) + self?.mostDroppedMusicList = musicList + case .failure(_): + output.mostDroppedMusicList.accept([]) + } + } + .disposed(by: disposedBag) + self?.recommendMusicUseCase.getArtistList() + .subscribe { result in + switch result { + case .success(let artists): + output.artists.accept(artists) + case .failure(_): + output.artists.accept([]) + } + } + .disposed(by: disposedBag) }) .disposed(by: disposedBag) @@ -95,9 +143,9 @@ final class DefaultSearchingMusicViewModel: SearchingMusicViewModel { } .disposed(by: disposedBag) - input.recommendQueryDidPressEvent - .bind { [weak self] recommendQuery in - self?.searchMusic(output: output, keyword: recommendQuery) + input.artistQueryDidPressEvent + .bind { [weak self] artistQuery in + self?.searchMusic(output: output, keyword: artistQuery) } .disposed(by: disposedBag) @@ -108,6 +156,29 @@ final class DefaultSearchingMusicViewModel: SearchingMusicViewModel { } .disposed(by: disposedBag) + input.musicDidPressEvent + .bind { music in + output.selectedMusic.accept(music) + } + .disposed(by: disposedBag) + + input.deletingButtonTappedEvent + .bind { [weak self] keyword in + guard let self else { return } + + Task { + await self.model.deleteRecentSearch(keyword: keyword) + + do { + let recentQueries = try await self.model.getRecentSearches().value + output.recentMusicQueries.accept(recentQueries) + } catch { + output.recentMusicQueries.accept([]) + } + } + } + .disposed(by: disposedBag) + return output }