├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── EventLogKit ├── EventLogKit.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── EventLogKit.xcscheme ├── EventLogKit │ ├── EventLog.h │ ├── EventLogType.swift │ ├── EventLogger.swift │ ├── Info.plist │ ├── ReachabilityObserver.swift │ └── StorageTypes.swift └── EventLogKitTests │ ├── EventLoggerTests.swift │ ├── Info.plist │ ├── Mock │ ├── MockReachabilityObservers.swift │ └── MockStorages.swift │ └── ReachabilityObserverTests.swift ├── MiniVibe ├── .swiftlint.yml ├── MiniVibe.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── MiniVibe.xcscheme │ │ └── MiniVibeTests.xcscheme ├── MiniVibe.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── MiniVibe │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 152.png │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 40-1.png │ │ │ ├── 40-2.png │ │ │ ├── 40.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── Contents.json │ │ │ ├── Icon-120.png │ │ │ ├── Icon-121.png │ │ │ ├── Icon-167.png │ │ │ ├── Icon-180.png │ │ │ ├── Icon-58.png │ │ │ ├── Icon-80.png │ │ │ └── Icon-87.png │ │ ├── Contents.json │ │ ├── PlayAndShuffleColor.colorset │ │ │ └── Contents.json │ │ ├── PreviewPlayerColor.colorset │ │ │ └── Contents.json │ │ ├── album.imageset │ │ │ ├── Contents.json │ │ │ └── album.jpg │ │ ├── artist.imageset │ │ │ ├── Contents.json │ │ │ └── artist.png │ │ ├── content.imageset │ │ │ ├── Contents.json │ │ │ └── content.png │ │ ├── magazine.imageset │ │ │ ├── Contents.json │ │ │ └── magazine.png │ │ ├── placeholder.imageset │ │ │ ├── Contents.json │ │ │ └── placeholder.png │ │ ├── playListThumbnail.imageset │ │ │ ├── Contents.json │ │ │ └── playListThumbnail.png │ │ ├── recommandedPlayListThumbnail.imageset │ │ │ ├── Contents.json │ │ │ └── recommandedPlayListThumbnail.png │ │ └── station.imageset │ │ │ ├── Contents.json │ │ │ └── station.png │ ├── Chart.swift │ ├── CoreData │ │ ├── Engagement+CoreDataClass.swift │ │ ├── Event.xcdatamodeld │ │ │ └── Event.xcdatamodel │ │ │ │ └── contents │ │ ├── LatestUpnext+CoreDataClass.swift │ │ ├── Like+CoreDataClass.swift │ │ ├── LogData.swift │ │ ├── MoveTrack+CoreDataClass.swift │ │ ├── Play+CoreDataClass.swift │ │ ├── PlayerDataManager.swift │ │ ├── Save+CoreDataClass.swift │ │ ├── Search+CoreDataClass.swift │ │ ├── Share+CoreDataClass.swift │ │ ├── Subscribe+CoreDataClass.swift │ │ ├── TrackInfoData.swift │ │ ├── Transition+CoreDataClass.swift │ │ ├── UpnextChange+CoreDataClass.swift │ │ └── User.xcdatamodeld │ │ │ └── User.xcdatamodel │ │ │ └── contents │ ├── EventLogger │ │ ├── ComponentId.swift │ │ ├── EventEndPoint.swift │ │ ├── EventLogResponse.swift │ │ ├── EventLogUseCase.swift │ │ ├── EventLogView.swift │ │ ├── EventLogViewModel.swift │ │ ├── Events │ │ │ ├── CustomEventLogType.swift │ │ │ ├── EngagementLog.swift │ │ │ ├── EventPrintable.swift │ │ │ ├── LikeLog.swift │ │ │ ├── MoveTrackLog.swift │ │ │ ├── PlayLog.swift │ │ │ ├── SaveLog.swift │ │ │ ├── SearchLog.swift │ │ │ ├── ShareLog.swift │ │ │ ├── SubscribeLog.swift │ │ │ ├── Transitions.swift │ │ │ └── UpnextChangeLog.swift │ │ ├── LocalEventStorage.swift │ │ ├── ServerEventStorage.swift │ │ ├── TransitionLogModifier.swift │ │ └── ViewIdentifier.swift │ ├── Extensions │ │ ├── CGFloat+Constant.swift │ │ ├── Color+CustomColor.swift │ │ ├── Date+timeStampFormat.swift │ │ ├── JSONEncoder+iso8601.swift │ │ ├── View+ColorScheme.swift │ │ └── View+dismissKeyboard.swift │ ├── Info.plist │ ├── Library.swift │ ├── MainTab.swift │ ├── MiniVibeApp.swift │ ├── Models │ │ ├── Albums.swift │ │ ├── Artist.swift │ │ ├── Magazines.swift │ │ ├── Mixtape.swift │ │ ├── News.swift │ │ ├── Playlists.swift │ │ ├── Station.swift │ │ ├── Track.swift │ │ ├── UseCases │ │ │ ├── AlbumUseCase.swift │ │ │ ├── ArtistUseCase.swift │ │ │ ├── ChartsUseCase.swift │ │ │ ├── EndPoint.swift │ │ │ ├── LibraryUseCase.swift │ │ │ ├── PlaylistUseCase.swift │ │ │ ├── SearchUseCase.swift │ │ │ ├── TodayUseCase.swift │ │ │ ├── TrackUseCase.swift │ │ │ └── UseCaseError.swift │ │ └── Utils │ │ │ └── NetworkService.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ViewModels │ │ ├── AlbumViewModel.swift │ │ ├── ArtistSectionViewModel.swift │ │ ├── ArtistViewModel.swift │ │ ├── ChartsViewModel.swift │ │ ├── LibraryViewModel.swift │ │ ├── MagazineViewModel.swift │ │ ├── MainTabViewModel.swift │ │ ├── NowPlayingViewModel.swift │ │ ├── PlaylistViewModel.swift │ │ ├── SearchViewModel.swift │ │ ├── TodayViewModel.swift │ │ └── TrackViewModel.swift │ └── Views │ │ ├── Album │ │ ├── AlbumSection.swift │ │ └── AlbumView.swift │ │ ├── Artist │ │ ├── ArtistSection.swift │ │ ├── ArtistView.swift │ │ └── Components │ │ │ ├── ArtistItem.swift │ │ │ └── ArtistThumbnail.swift │ │ ├── Common │ │ ├── ChartList.swift │ │ ├── ChartSections │ │ │ ├── ChartSectionA.swift │ │ │ └── ChartSectionB.swift │ │ ├── Components │ │ │ ├── PlayAndShuffle.swift │ │ │ ├── PlaylistAlbumInfo.swift │ │ │ ├── SectionTitle.swift │ │ │ └── ThumbnailItem.swift │ │ ├── Menu │ │ │ ├── AlbumMenu.swift │ │ │ ├── ArtistMenu.swift │ │ │ ├── Components │ │ │ │ └── MenuAlbumImage.swift │ │ │ ├── MenuButton.swift │ │ │ ├── MenuCloseButton.swift │ │ │ ├── MenuThumbnailButton.swift │ │ │ ├── PlayListMenu.swift │ │ │ └── PlayerMenu.swift │ │ ├── MultiselectTabBar.swift │ │ ├── PlayerPreview.swift │ │ ├── TabbarButton.swift │ │ ├── ThumbnailGrid.swift │ │ ├── ThumbnailGridView.swift │ │ ├── ThumbnailList.swift │ │ ├── ThumbnailRow.swift │ │ └── TrackRows │ │ │ ├── Components │ │ │ ├── TrackRowImage.swift │ │ │ └── TrackRowInfo.swift │ │ │ ├── TrackRowA.swift │ │ │ ├── TrackRowB.swift │ │ │ ├── TrackRowC.swift │ │ │ ├── TrackRowD.swift │ │ │ └── TrackRowE.swift │ │ ├── Library │ │ ├── LibraryAlbumsView.swift │ │ ├── LibraryArtistRow.swift │ │ ├── LibraryArtistsView.swift │ │ ├── LibraryPlayListRow.swift │ │ ├── LibraryPlayListView.swift │ │ └── LibrarySongsView.swift │ │ ├── Mixtape │ │ ├── MixtapeGrid.swift │ │ └── MixtapeSection.swift │ │ ├── PlayList │ │ ├── PlayListSection.swift │ │ └── PlayListView.swift │ │ ├── Player │ │ ├── Components │ │ │ ├── PlayerControls.swift │ │ │ ├── PlayerHeader.swift │ │ │ ├── PlayerSlider.swift │ │ │ ├── PlayerSliderView.swift │ │ │ ├── PlayerThumbnail.swift │ │ │ └── RepeatButton.swift │ │ ├── Lyrics.swift │ │ ├── Player.swift │ │ ├── PlayerView.swift │ │ └── UpNextList.swift │ │ ├── Profile │ │ ├── Article.swift │ │ └── ProfileView.swift │ │ ├── Search │ │ ├── Components │ │ │ └── NewsItem.swift │ │ ├── GenreSection.swift │ │ ├── NewsSection.swift │ │ ├── SearchBar.swift │ │ └── SearchView.swift │ │ └── Today │ │ ├── Components │ │ ├── MagazineItem.swift │ │ ├── PreviewItem.swift │ │ ├── RecommandedPlayListItem.swift │ │ └── StationItem.swift │ │ ├── MagazineSection.swift │ │ ├── MagazineView.swift │ │ ├── PreviewSection.swift │ │ ├── RecommandedPlayListSection.swift │ │ ├── StationList.swift │ │ ├── StationSection.swift │ │ ├── StationStack.swift │ │ ├── Today.swift │ │ └── TodayTitle.swift ├── MiniVibeTests │ ├── AlbumUseCaseTests.swift │ ├── AlbumViewModelTests.swift │ ├── ArtistSectionViewModelTests.swift │ ├── ArtistViewModelTests.swift │ ├── ArtistsUseCaseTests.swift │ ├── ChartUseCaseTests.swift │ ├── ChartsViewModelTests.swift │ ├── Extensions │ │ ├── AlbumExtensions.swift │ │ ├── AlbumViewModelExtensions.swift │ │ ├── ArtistsExtensions.swift │ │ ├── MagazineExtensions.swift │ │ ├── MixtapeExtensions.swift │ │ ├── NetworkError+Equatable.swift │ │ ├── NewsExtensions.swift │ │ ├── PlaylistExtensions.swift │ │ ├── PlaylistViewModelExtensions.swift │ │ └── TrackExtensions.swift │ ├── Info.plist │ ├── LibraryUseCaseTests.swift │ ├── LibraryViewModelTests.swift │ ├── LocalEventStorageTests.swift │ ├── MainTabViewModelTests.swift │ ├── Mock │ │ ├── LibraryUseCase.swift │ │ ├── MockAlbumUseCase.swift │ │ ├── MockArtistSectionUseCase.swift │ │ ├── MockChartsUseCase.swift │ │ ├── MockEventLogger.swift │ │ ├── MockNetworkServices.swift │ │ ├── MockPlayerDataManager.swift │ │ ├── MockSearchUseCase.swift │ │ ├── MockTodayUseCase.swift │ │ └── MockTrackUseCase.swift │ ├── NetworkServiceTests.swift │ ├── NowPlayingViewModelTests.swift │ ├── PlaylistUseCaseTests.swift │ ├── PlaylistViewModelTests.swift │ ├── SearchUseCaseTests.swift │ ├── SearchViewModelTests.swift │ ├── TodayUseCaseTests.swift │ ├── TodayViewModelTests.swift │ ├── TrackUseCaseTests.swift │ └── TrackViewModelTests.swift ├── Podfile └── Podfile.lock ├── README.md ├── client ├── .babelrc ├── .eslintrc.js ├── .prettierrc ├── .storybook │ ├── main.js │ └── preview.js ├── assets │ └── global.css ├── components │ ├── atoms │ │ ├── A │ │ │ ├── A.stories.tsx │ │ │ ├── A.styles.tsx │ │ │ ├── A.tsx │ │ │ └── index.ts │ │ ├── Button │ │ │ ├── AddButton.tsx │ │ │ ├── Button.stories.tsx │ │ │ ├── Button.styles.tsx │ │ │ ├── Button.tsx │ │ │ └── index.ts │ │ ├── CheckBox │ │ │ ├── CheckBox.stories.tsx │ │ │ └── index.tsx │ │ ├── Circle │ │ │ ├── Circle.stories.tsx │ │ │ ├── Circle.styles.tsx │ │ │ └── Circle.tsx │ │ ├── Heart │ │ │ ├── Heart.stories.tsx │ │ │ └── Heart.tsx │ │ ├── IconButton │ │ │ ├── IconButton.stories.tsx │ │ │ ├── IconButton.styles.tsx │ │ │ ├── IconButton.tsx │ │ │ └── index.ts │ │ ├── Image │ │ │ ├── Image.stories.tsx │ │ │ ├── Image.styles.tsx │ │ │ └── Image.tsx │ │ ├── Input │ │ │ ├── Input.stories.tsx │ │ │ ├── Input.styles.tsx │ │ │ └── Input.tsx │ │ ├── Label │ │ │ ├── Label.stories.tsx │ │ │ ├── Label.styles.tsx │ │ │ ├── Label.tsx │ │ │ └── index.ts │ │ ├── MenuLink │ │ │ ├── MenuLink.stories.tsx │ │ │ ├── MenuLink.tsx │ │ │ └── index.ts │ │ ├── NaverLoginButton │ │ │ └── index.tsx │ │ ├── PlayTriangle │ │ │ ├── PlayTriangle.stories.tsx │ │ │ ├── PlayTriangle.styles.tsx │ │ │ └── PlayTriangle.tsx │ │ ├── Playtime │ │ │ ├── Playtime.stories.tsx │ │ │ └── index.tsx │ │ └── Text │ │ │ ├── HiddenText.tsx │ │ │ ├── Text.stories.tsx │ │ │ ├── Text.styles.tsx │ │ │ └── index.tsx │ ├── molecules │ │ ├── ArtistLikeButton │ │ │ ├── ArtistLikeButton.stories.tsx │ │ │ ├── ArtistLikeButton.styles.tsx │ │ │ └── ArtistLikeButton.tsx │ │ ├── ArtistThumbnail │ │ │ ├── LibraryArtistThumbnail │ │ │ │ ├── LibraryArtistThumbnail.stories.tsx │ │ │ │ └── LibraryArtistThumbnail.tsx │ │ │ └── NormalArtistThumbnail │ │ │ │ ├── NormalArtistThumbnail.stories.tsx │ │ │ │ └── NormalArtistThumbnail.tsx │ │ ├── ChartCard │ │ │ ├── ChartCard.stories.tsx │ │ │ ├── ChartCard.styles.tsx │ │ │ └── ChartCard.tsx │ │ ├── ContentsThumbnail │ │ │ ├── ContentsThumbnail.stories.tsx │ │ │ └── ContentsThumbnail.tsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.stories.tsx │ │ │ └── index.tsx │ │ ├── GenreCard │ │ │ ├── GenreCard.stories.tsx │ │ │ ├── GenreCard.tsx │ │ │ └── index.tsx │ │ ├── LoginForm │ │ │ └── index.tsx │ │ ├── MainMenu │ │ │ ├── MainMenu.stories.tsx │ │ │ └── index.tsx │ │ ├── NoDataContainer │ │ │ ├── NoDataContainer.stories.tsx │ │ │ └── index.tsx │ │ ├── PlayButton │ │ │ ├── PlayButton.stories.tsx │ │ │ ├── PlayButton.styles.tsx │ │ │ └── PlayButton.tsx │ │ ├── PlayControllerButtons │ │ │ ├── PlayControllerButtons.stories.tsx │ │ │ └── index.tsx │ │ ├── PlaylistDisplayButton │ │ │ ├── PlaylistButton.stories.tsx │ │ │ └── index.tsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.stories.tsx │ │ │ └── index.tsx │ │ ├── SearchInput │ │ │ ├── SearchInput.stories.tsx │ │ │ ├── SearchInput.tsx │ │ │ └── index.ts │ │ ├── SlideNextButton │ │ │ ├── SlideNextButton.stories.tsx │ │ │ ├── SlideNextButton.styles.tsx │ │ │ └── index.tsx │ │ ├── SlidePrevButton │ │ │ ├── SlidePrevButton.stories.tsx │ │ │ ├── SlidePrevButton.styles.tsx │ │ │ └── index.tsx │ │ ├── SubMenu │ │ │ ├── SubMenu.stories.tsx │ │ │ └── index.tsx │ │ ├── TrackCard │ │ │ ├── TrackCard.stories.tsx │ │ │ └── index.tsx │ │ ├── TrackInfo │ │ │ ├── TrackInfo.stories.tsx │ │ │ └── index.tsx │ │ ├── TrackPlayButton │ │ │ ├── TrackPlayButton.stories.tsx │ │ │ ├── TrackPlayButton.styles.tsx │ │ │ └── index.tsx │ │ └── VolumnController │ │ │ ├── VolumnController.stories.tsx │ │ │ └── index.tsx │ └── organisms │ │ ├── ArtistHeader │ │ ├── ArtistHeader.stories.tsx │ │ └── ArtistHeader.tsx │ │ ├── CardListContainer │ │ ├── CardListContainer.stories.tsx │ │ ├── CardListContainer.tsx │ │ └── index.ts │ │ ├── CardLists │ │ ├── ChartCardList │ │ │ ├── ChartCardList.stories.tsx │ │ │ └── index.tsx │ │ ├── ContentsCardList │ │ │ ├── ContentsCardList.stories.tsx │ │ │ ├── ContentsCardList.tsx │ │ │ └── index.tsx │ │ ├── GenreCardList │ │ │ ├── GenreCardList.stories.tsx │ │ │ └── index.tsx │ │ ├── MagazineList │ │ │ ├── MagazineList.stories.tsx │ │ │ ├── MagazineList.styles.tsx │ │ │ └── MagazineList.tsx │ │ ├── PlayerTrackList │ │ │ ├── PlayerTrackList.stories.tsx │ │ │ └── index.tsx │ │ ├── StationCardList │ │ │ ├── StationCard.stories.tsx │ │ │ └── StationCardList.tsx │ │ └── TrackRowList │ │ │ ├── TrackRowList.stories.tsx │ │ │ └── index.tsx │ │ ├── CardScrollList │ │ ├── CardScrollList.stories.tsx │ │ ├── CardScrollList.styles.tsx │ │ ├── CardScrollList.tsx │ │ └── index.ts │ │ ├── Cards │ │ ├── AlbumCard │ │ │ ├── AlbumCard.stories.tsx │ │ │ └── AlbumCard.tsx │ │ ├── MagazineCard │ │ │ ├── MagazineCard.stories.tsx │ │ │ ├── MagazineCard.tsx │ │ │ └── index.ts │ │ ├── MainMagazineCard │ │ │ ├── MainMagazineCard.stories.tsx │ │ │ └── MainMagazineCard.tsx │ │ ├── MixtapeCard │ │ │ ├── MixtapeCard.stories.tsx │ │ │ └── MixtapeCard.tsx │ │ ├── NewsCard │ │ │ ├── NewsCard.stories.tsx │ │ │ ├── NewsCard.tsx │ │ │ └── index.ts │ │ ├── PlayerTrackCard │ │ │ ├── PlayerTrackCard.stories.tsx │ │ │ └── index.tsx │ │ ├── PlaylistCard │ │ │ ├── PlaylistCard.stories.tsx │ │ │ └── PlaylistCard.tsx │ │ ├── StationCard │ │ │ ├── StationCard.stories.tsx │ │ │ └── StationCard.tsx │ │ └── TrackRowCard │ │ │ ├── TrackRowCard.stories.tsx │ │ │ ├── TrackRowCard.styles.tsx │ │ │ └── index.tsx │ │ ├── ContentsButtonGroup │ │ ├── ContentsButtonGroup.stories.tsx │ │ └── index.tsx │ │ ├── DetailHeader │ │ ├── DetailHeader.stories.tsx │ │ └── index.tsx │ │ ├── FloatingSelectMenu │ │ ├── FloatingSelectMenu.stories.tsx │ │ └── index.tsx │ │ ├── HeaderButtonGroup │ │ ├── HeaderButtonGroup.stories.tsx │ │ └── index.tsx │ │ ├── HeaderSideBar │ │ ├── HeaderSideBar.stories.tsx │ │ ├── HeaderSideBar.styles.tsx │ │ └── index.tsx │ │ ├── Library │ │ ├── LibraryCardList │ │ │ ├── LibraryCardList.stories.tsx │ │ │ ├── LibraryCardList.styles.tsx │ │ │ └── LibraryCardList.tsx │ │ └── LibraryHeader │ │ │ ├── LibraryHeader.stories.tsx │ │ │ └── LibraryHeader.tsx │ │ ├── LyricModal │ │ ├── LyricModal.stories.tsx │ │ └── LyricModal.tsx │ │ ├── MusicPlayer │ │ ├── MusicPlayer.stories.tsx │ │ ├── PlayController │ │ │ ├── PlayController.stories.tsx │ │ │ └── index.tsx │ │ ├── PlayerTrackInfo │ │ │ ├── PlayerTrackInfo.stories.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ │ ├── PlaylistModal │ │ ├── NewPlaylistButton │ │ │ ├── NewPlaylistButton.stories.tsx │ │ │ └── index.tsx │ │ ├── PlaylistAddModal │ │ │ ├── PlaylistAddModal.stories.tsx │ │ │ └── index.tsx │ │ ├── PlaylistModal.stories.tsx │ │ ├── PlaylistRowCard │ │ │ ├── PlaylistRowCard.stories.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ │ └── UserProfileMenu │ │ ├── UserProfileMenu.stories.tsx │ │ ├── UserProfileMenu.tsx │ │ └── index.ts ├── constants │ ├── actions.ts │ ├── apiUrl.ts │ ├── dropDownMenu.ts │ ├── events.ts │ └── identifier.ts ├── hooks │ ├── useClickEventLog.ts │ ├── useDropDownAction.ts │ ├── useInput.ts │ ├── useLikeEventLog.ts │ ├── usePlayEventLog.ts │ ├── usePlayNowEventLog.ts │ ├── useSearchEventLog.ts │ ├── useTransitionEventLog.ts │ └── useUpNextChangeEventLog.ts ├── interfaces │ ├── eventProps.ts │ └── props.ts ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── admin.tsx │ ├── album │ │ └── [id].tsx │ ├── artist │ │ └── [id].tsx │ ├── chart.tsx │ ├── dj-station.tsx │ ├── genre │ │ └── sample.tsx │ ├── index.tsx │ ├── join.tsx │ ├── library │ │ ├── albums.tsx │ │ ├── artists.tsx │ │ ├── mixtapes.tsx │ │ ├── playlists.tsx │ │ └── tracks.tsx │ ├── login │ │ └── index.tsx │ ├── magazine │ │ └── [id].tsx │ ├── magazines.tsx │ ├── mixtape │ │ └── sample.tsx │ ├── playlist │ │ └── [id].tsx │ ├── this-month.tsx │ └── track │ │ └── [id].tsx ├── reducers │ ├── index.ts │ ├── musicPlayer.ts │ ├── playlist.ts │ ├── selectedTrack.ts │ └── user.ts ├── sagas │ ├── index.ts │ ├── musicPlayer.ts │ └── user.ts ├── store │ └── configureStore.ts ├── tsconfig.json └── utils │ ├── EventLogController.ts │ ├── Timer.ts │ ├── api.js │ ├── apis.js │ ├── color.ts │ ├── context │ ├── ComponentInfoContext.ts │ └── ComponentInfoWrapper.tsx │ ├── cookies.ts │ ├── eventLogger.ts │ └── time.ts ├── eventSever ├── .eslintrc.js ├── .prettierrc ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── db.ts │ ├── models │ │ ├── Event.ts │ │ └── PlayEvent.ts │ ├── routes │ │ ├── events │ │ │ ├── events.controller.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── playEvents │ │ │ ├── index.ts │ │ │ └── playEvents.controller.ts │ └── types │ │ └── event.ts └── tsconfig.json └── server ├── .eslintrc.js ├── .prettierrc ├── package-lock.json ├── package.json ├── src ├── app.ts ├── db.ts ├── middlewares │ └── auth.ts ├── models │ ├── Album.ts │ ├── Artist.ts │ ├── Event.ts │ ├── Genre.ts │ ├── Magazine.ts │ ├── News.ts │ ├── PlayEvent.ts │ ├── Playlist.ts │ ├── Track.ts │ └── User.ts ├── routes │ ├── albums │ │ ├── albums.controller.ts │ │ └── index.ts │ ├── artists │ │ ├── artists.controller.ts │ │ └── index.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── index.ts │ │ └── passport │ │ │ ├── jwt-strategy.ts │ │ │ ├── local-strategy.ts │ │ │ ├── naver-strategy.ts │ │ │ └── passport-init.ts │ ├── chart │ │ ├── chart.controller.ts │ │ └── index.ts │ ├── genres │ │ ├── genres.controller.ts │ │ └── index.ts │ ├── index.ts │ ├── library │ │ ├── album │ │ │ ├── index.ts │ │ │ └── library.album.controller.ts │ │ ├── artist │ │ │ ├── index.ts │ │ │ └── library.artist.controller.ts │ │ ├── index.ts │ │ ├── mixtapes │ │ │ ├── index.ts │ │ │ └── library.mixtapes.controller.ts │ │ ├── playlists │ │ │ ├── index.ts │ │ │ └── library.playlists.controller.ts │ │ └── tracks │ │ │ ├── index.ts │ │ │ └── library.tracks.controller.ts │ ├── magazines │ │ ├── index.ts │ │ └── magazines.controller.ts │ ├── mixtapes │ │ ├── index.ts │ │ └── mixtapes.controller.ts │ ├── news │ │ ├── index.ts │ │ └── news.controller.ts │ ├── playlists │ │ ├── index.ts │ │ └── playlists.controller.ts │ ├── tracks │ │ ├── index.ts │ │ └── tracks.controller.ts │ └── users │ │ ├── index.ts │ │ └── users.controller.ts ├── types │ └── event.ts └── utils │ └── getRandomImage.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🗣 설명 11 | 12 | 13 | 14 | ## 📋 체크리스트 15 | 16 | > 구현해야하는 이슈 체크리스트 17 | 18 | - [ ] 체크 사항 1 19 | - [ ] 체크 사항 2 20 | - [ ] 체크 사항 3 21 | - [ ] 체크 사항 4 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 📕 Issue Number 2 | 3 | Close # 4 | 5 | 6 | ### 📙 작업 내역 7 | 8 | > 구현 내용 및 작업 했던 내역 9 | 10 | - [ ] 작업 내역 1 11 | - [ ] 작업 내역 2 12 | - [ ] 작업 내역 3 13 | - [ ] 작업 내역 4 14 | 15 | 16 | ### 📘 작업 유형 17 | 18 | - [x] 신규 기능 추가 19 | - [ ] 버그 수정 20 | - [ ] 리펙토링 21 | - [ ] 문서 업데이트 22 | 23 | 24 | ### 📋 체크리스트 25 | 26 | - [ ] Merge 하는 브랜치가 올바른가? 27 | - [ ] 코딩컨벤션을 준수하는가? 28 | - [ ] PR과 관련없는 변경사항이 없는가? 29 | - [ ] 내 코드에 대한 자기 검토가 되었는가? 30 | - [ ] 변경사항이 효과적이거나 동작이 작동한다는 것을 보증하는 테스트를 추가하였는가? 31 | - [ ] 새로운 테스트와 기존의 테스트가 변경사항에 대해 만족하는가? 32 |
33 | 34 | ### 📝 PR 특이 사항 35 | 36 | > PR을 볼 때 주의깊게 봐야하거나 말하고 싶은 점 37 | 38 | - 특이 사항 1 39 | - 특이 사항 2 40 | 41 |

42 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Reachability", 6 | "repositoryURL": "https://github.com/ashleymills/Reachability.swift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", 10 | "version": "5.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit/EventLog.h: -------------------------------------------------------------------------------- 1 | // 2 | // EventLog.h 3 | // EventLog 4 | // 5 | // Created by TTOzzi on 2020/12/12. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for EventLog. 11 | FOUNDATION_EXPORT double EventLogVersionNumber; 12 | 13 | //! Project version string for EventLog. 14 | FOUNDATION_EXPORT const unsigned char EventLogVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit/EventLogType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventLogType.swift 3 | // EventLog 4 | // 5 | // Created by TTOzzi on 2020/12/12. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | public protocol EventLogType: Encodable { 12 | var event: String { get } 13 | var timestamp: Date { get } 14 | func save(context: NSManagedObjectContext) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKit/StorageTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageTypes.swift 3 | // EventLog 4 | // 5 | // Created by TTOzzi on 2020/12/13. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol LocalStorageType { 11 | func save(_ event: EventLogType) 12 | func sendToServer() 13 | } 14 | 15 | public protocol ServerStorageType { 16 | func send(_ event: T) 17 | func setFailureHandler(_ handler: @escaping (EventLogType) -> Void) 18 | } 19 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKitTests/Mock/MockReachabilityObservers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockReachabilityObservers.swift 3 | // EventLogKitTests 4 | // 5 | // Created by TTOzzi on 2020/12/13. 6 | // 7 | 8 | import Foundation 9 | @testable import EventLogKit 10 | 11 | struct AvailableReachability: ReachabilityObserving { 12 | var hostName: String = "test" 13 | var state: Connection = .unavailable 14 | 15 | func setUpNotify(_ action: ((Connection) -> Void)?) { 16 | action?(.wifi) 17 | } 18 | } 19 | 20 | struct UnavailableReachability: ReachabilityObserving { 21 | var hostName: String = "test" 22 | var state: Connection = .unavailable 23 | 24 | func setUpNotify(_ action: ((Connection) -> Void)?) { 25 | action?(.unavailable) 26 | } 27 | } 28 | 29 | final class MockReachability: ReachabilityObserving { 30 | var hostName: String = "test" 31 | var state: Connection = .unavailable { 32 | didSet { 33 | action?(state) 34 | } 35 | } 36 | var action: ((Connection) -> Void)? 37 | 38 | func setUpNotify(_ action: ((Connection) -> Void)?) { 39 | self.action = action 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EventLogKit/EventLogKitTests/Mock/MockStorages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockStorages.swift 3 | // EventLogKitTests 4 | // 5 | // Created by TTOzzi on 2020/12/13. 6 | // 7 | 8 | import Foundation 9 | @testable import EventLogKit 10 | 11 | struct MockLocalStorage: LocalStorageType { 12 | let saveHandler: (EventLogType) -> Void 13 | let sendToServerHandler: () -> Void 14 | 15 | func save(_ event: EventLogType) { 16 | saveHandler(event) 17 | } 18 | 19 | func sendToServer() { 20 | sendToServerHandler() 21 | } 22 | } 23 | 24 | struct MockServerStorage: ServerStorageType { 25 | let sendHandler: (EventLogType) -> Void 26 | 27 | func send(_ event: T) where T : EventLogType { 28 | sendHandler(event) 29 | } 30 | 31 | func setFailureHandler(_ handler: @escaping (EventLogType) -> Void) { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MiniVibe/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - identifier_name 4 | - weak_delegate 5 | - nesting 6 | - function_body_length 7 | - xctfail_message 8 | excluded: 9 | - Pods 10 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Kingfisher", 6 | "repositoryURL": "https://github.com/onevcat/Kingfisher.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "2a10bf41da75599a9f8e872dbd44fe0155a2e00c", 10 | "version": "5.15.8" 11 | } 12 | }, 13 | { 14 | "package": "Reachability", 15 | "repositoryURL": "https://github.com/ashleymills/Reachability.swift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", 19 | "version": "5.1.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.362", 9 | "green" : "0.004", 10 | "red" : "0.999" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/40-1.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/40-2.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-120.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-121.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-167.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-180.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-58.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-80.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/AppIcon.appiconset/Icon-87.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/album.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "album.jpg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/album.imageset/album.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/album.imageset/album.jpg -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/artist.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "artist.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/artist.imageset/artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/artist.imageset/artist.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "content.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/content.imageset/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/content.imageset/content.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/magazine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "magazine.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/magazine.imageset/magazine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/magazine.imageset/magazine.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "placeholder.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/placeholder.imageset/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/placeholder.imageset/placeholder.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/playListThumbnail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "playListThumbnail.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/playListThumbnail.imageset/playListThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/playListThumbnail.imageset/playListThumbnail.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/recommandedPlayListThumbnail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "recommandedPlayListThumbnail.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/recommandedPlayListThumbnail.imageset/recommandedPlayListThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/recommandedPlayListThumbnail.imageset/recommandedPlayListThumbnail.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/station.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "station.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Assets.xcassets/station.imageset/station.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project01-B-User-Event-Collector/002eb40b100b836eb81c5ef8723199d51e7d472b/MiniVibe/MiniVibe/Assets.xcassets/station.imageset/station.png -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/CoreData/LatestUpnext+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LatestUpnext+CoreDataClass.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/14. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(LatestUpnext) 13 | public class LatestUpnext: NSManagedObject { 14 | 15 | } 16 | 17 | extension LatestUpnext { 18 | 19 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 20 | return NSFetchRequest(entityName: "LatestUpnext") 21 | } 22 | 23 | @NSManaged public var track: [TrackInfoData] 24 | 25 | } 26 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/CoreData/LogData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogData.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/11. 6 | // 7 | import Foundation 8 | 9 | @objc(LogData) 10 | public final class LogData: NSSecureUnarchiveFromDataTransformer, NSCoding, Encodable { 11 | public let type: String 12 | public let id: Int 13 | 14 | public init(type: String, id: Int) { 15 | self.type = type 16 | self.id = id 17 | } 18 | 19 | public required init?(coder: NSCoder) { 20 | type = (coder.decodeObject(forKey: "type") as? String) ?? "none" 21 | id = coder.decodeInteger(forKey: "id") 22 | } 23 | 24 | public func encode(with coder: NSCoder) { 25 | coder.encode(type, forKey: "type") 26 | coder.encode(id, forKey: "id") 27 | } 28 | } 29 | 30 | extension LogData { 31 | public override var description: String { 32 | return "\(type)-\(id)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/CoreData/User.xcdatamodeld/User.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/EventEndPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventEndPoint.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum EventEndPoint { 11 | 12 | case play 13 | case common 14 | 15 | static private let baseURL = "http://49.50.174.152:5500" 16 | private var path: String { 17 | switch self { 18 | case .play: 19 | return "/api/play-events" 20 | case .common: 21 | return "/api/events" 22 | } 23 | } 24 | 25 | var urlString: String { 26 | return Self.baseURL + path 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/EventLogResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventLogResponse.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct EventLogResponse: Decodable { 11 | let data: [EventLog] 12 | } 13 | 14 | struct EventLog: Decodable { 15 | var event: String? 16 | var platform: String 17 | var timestamp: String 18 | } 19 | 20 | extension EventLog: CustomStringConvertible { 21 | var description: String { 22 | return "\(timestamp)\n\(event ?? "none")" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/CustomEventLogType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomEventLogType.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/15. 6 | // 7 | 8 | import Foundation 9 | import EventLogKit 10 | 11 | protocol CustomEventLogType: EventLogType { 12 | var userId: Int { get } 13 | var platform: String { get } 14 | } 15 | 16 | extension CustomEventLogType { 17 | var description: String { 18 | return "\(timestamp.timestampFormat())\n\(event)" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/EngagementLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Engagement.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/11. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import EventLogKit 11 | 12 | protocol EngagementLogType: CustomEventLogType { 13 | 14 | } 15 | 16 | extension EngagementLogType { 17 | func save(context: NSManagedObjectContext) { 18 | let engagement = Engagement(context: context) 19 | engagement.event = event 20 | engagement.userId = userId 21 | engagement.timestamp = timestamp 22 | engagement.platform = platform 23 | try? context.save() 24 | } 25 | } 26 | 27 | struct Active: EngagementLogType { 28 | let userId: Int 29 | let timestamp = Date() 30 | let event = "Active" 31 | let platform = "iOS" 32 | } 33 | 34 | struct Background: EngagementLogType { 35 | let userId: Int 36 | let timestamp = Date() 37 | let event = "Background" 38 | let platform = "iOS" 39 | } 40 | 41 | struct Terminate: EngagementLogType { 42 | let userId: Int 43 | let timestamp = Date() 44 | let event = "Terminate" 45 | let platform = "iOS" 46 | } 47 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/EventPrintable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventPrintable.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol EventPrintable: CustomStringConvertible { 11 | var event: String { get } 12 | var timestamp: Date { get } 13 | } 14 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/LikeLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LikeLog.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/10. 6 | // 7 | 8 | import CoreData 9 | import EventLogKit 10 | 11 | protocol LikeLogType: CustomEventLogType { 12 | var componentId: String { get } 13 | var data: LogData { get } 14 | var isLike: Bool { get } 15 | } 16 | 17 | extension LikeLogType { 18 | func save(context: NSManagedObjectContext) { 19 | let likeLog = Like(context: context) 20 | likeLog.event = event 21 | likeLog.userId = userId 22 | likeLog.componentId = componentId 23 | likeLog.data = data 24 | likeLog.isLike = isLike 25 | likeLog.timestamp = timestamp 26 | likeLog.platform = platform 27 | try? context.save() 28 | } 29 | } 30 | 31 | struct LikeLog: LikeLogType { 32 | let userId: Int 33 | let componentId: String = "likeButton" 34 | var data: LogData 35 | var isLike: Bool 36 | let timestamp: Date = Date() 37 | let event = "Like" 38 | let platform = "iOS" 39 | } 40 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/MoveTrackLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTrackLog.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import EventLogKit 11 | 12 | protocol MoveTrackLogType: CustomEventLogType { 13 | var trackId: Int { get } 14 | var source: Int { get } 15 | var destination: Int { get } 16 | } 17 | 18 | extension MoveTrackLogType { 19 | func save(context: NSManagedObjectContext) { 20 | let moveTrack = MoveTrack(context: context) 21 | moveTrack.event = event 22 | moveTrack.userId = userId 23 | moveTrack.trackId = trackId 24 | moveTrack.source = source 25 | moveTrack.destination = destination 26 | moveTrack.timestamp = timestamp 27 | moveTrack.platform = platform 28 | try? context.save() 29 | } 30 | } 31 | 32 | struct MoveTrackLog: MoveTrackLogType { 33 | let userId: Int 34 | let trackId: Int 35 | let source: Int 36 | let destination: Int 37 | let timestamp = Date() 38 | let event = "MoveTrack" 39 | let platform = "iOS" 40 | } 41 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/PlayLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayLog.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/11. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import EventLogKit 11 | 12 | protocol PlayLogType: CustomEventLogType { 13 | var trackId: Int { get } 14 | var componentId: String { get } 15 | var isPlay: Bool { get } 16 | } 17 | 18 | extension PlayLogType { 19 | func save(context: NSManagedObjectContext) { 20 | let play = Play(context: context) 21 | play.event = event 22 | play.userId = userId 23 | play.timestamp = timestamp 24 | play.trackId = trackId 25 | play.componentId = componentId 26 | play.isPlay = isPlay 27 | play.platform = platform 28 | try? context.save() 29 | } 30 | } 31 | 32 | struct PlayLog: PlayLogType { 33 | let userId: Int 34 | let trackId: Int 35 | let componentId: String 36 | var isPlay: Bool 37 | let timestamp = Date() 38 | let event = "Play" 39 | let platform = "iOS" 40 | } 41 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/SaveLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveLog.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/11. 6 | // 7 | 8 | import CoreData 9 | import EventLogKit 10 | 11 | protocol SaveLogType: CustomEventLogType { 12 | var componentId: String { get } 13 | var data: LogData { get } 14 | } 15 | 16 | extension SaveLogType { 17 | func save(context: NSManagedObjectContext) { 18 | let saveLog = Save(context: context) 19 | saveLog.event = event 20 | saveLog.userId = userId 21 | saveLog.timestamp = timestamp 22 | saveLog.componentId = componentId 23 | saveLog.data = data 24 | saveLog.platform = platform 25 | try? context.save() 26 | } 27 | } 28 | 29 | struct SaveLog: SaveLogType { 30 | let userId: Int 31 | let componentId: String 32 | let data: LogData 33 | let timestamp = Date() 34 | let event = "Save" 35 | let platform = "iOS" 36 | } 37 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/SearchLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchLog.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/10. 6 | // 7 | 8 | import CoreData 9 | import EventLogKit 10 | 11 | protocol SearchLogType: CustomEventLogType { 12 | var componentId: String { get } 13 | var text: String { get } 14 | } 15 | 16 | extension SearchLogType { 17 | func save(context: NSManagedObjectContext) { 18 | let searchLog = Search(context: context) 19 | searchLog.event = event 20 | searchLog.userId = userId 21 | searchLog.componentId = componentId 22 | searchLog.text = text 23 | searchLog.timestamp = timestamp 24 | searchLog.platform = platform 25 | try? context.save() 26 | } 27 | } 28 | 29 | struct SearchLog: SearchLogType { 30 | let userId: Int 31 | let componentId: String 32 | let text: String 33 | let timestamp: Date = Date() 34 | let event = "Search" 35 | let platform = "iOS" 36 | } 37 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/ShareLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareLog.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/11. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | import EventLogKit 11 | 12 | protocol ShareLogType: CustomEventLogType { 13 | var componentId: String { get } 14 | var data: LogData { get } 15 | } 16 | 17 | extension ShareLogType { 18 | func save(context: NSManagedObjectContext) { 19 | let shareLog = Share(context: context) 20 | shareLog.event = event 21 | shareLog.userId = userId 22 | shareLog.componentId = componentId 23 | shareLog.data = data 24 | shareLog.timestamp = timestamp 25 | shareLog.platform = platform 26 | try? context.save() 27 | } 28 | } 29 | 30 | struct ShareLog: ShareLogType { 31 | let userId: Int 32 | let componentId: String 33 | let data: LogData 34 | let timestamp: Date = Date() 35 | let event: String = "Share" 36 | let platform: String = "iOS" 37 | } 38 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/EventLogger/Events/SubscribeLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subscribe.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/10. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | import EventLogKit 11 | 12 | protocol SubscribeLogType: CustomEventLogType { 13 | var componentId: String { get } 14 | } 15 | 16 | extension SubscribeLogType { 17 | func save(context: NSManagedObjectContext) { 18 | let subscribe = Subscribe(context: context) 19 | subscribe.event = event 20 | subscribe.userId = userId 21 | subscribe.componentId = componentId 22 | subscribe.timestamp = timestamp 23 | subscribe.platform = platform 24 | try? context.save() 25 | } 26 | } 27 | 28 | struct SubscribeLog: SubscribeLogType { 29 | let userId: Int 30 | let timestamp = Date() 31 | let componentId: String 32 | let event = "Subscribe" 33 | let platform = "iOS" 34 | } 35 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Extensions/CGFloat+Constant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat+Constant.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/25. 6 | // 7 | 8 | import UIKit 9 | 10 | extension CGFloat { 11 | static let spacingRatio: CGFloat = 0.02 12 | static let paddingRatio: CGFloat = 0.04 13 | static let thumbnailRatio: CGFloat = 0.45 14 | static let sectionRatio: CGFloat = 0.92 15 | } 16 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Extensions/Color+CustomColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+CustomColor.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color { 11 | 12 | static var playAndShuffle: Color { 13 | return Color("PlayAndShuffleColor") 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Extensions/Date+timeStampFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+timeStampFormat.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | func timestampFormat() -> String { 12 | let formatter = DateFormatter() 13 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 14 | formatter.timeZone = .current 15 | return formatter.string(from: self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Extensions/JSONEncoder+iso8601.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONEncoder+iso8601.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Formatter { 11 | static let iso8601Custom: DateFormatter = { 12 | let formatter = DateFormatter() 13 | formatter.calendar = Calendar(identifier: .iso8601) 14 | formatter.locale = .current 15 | formatter.timeZone = .current 16 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" 17 | return formatter 18 | }() 19 | } 20 | 21 | extension JSONEncoder.DateEncodingStrategy { 22 | static let iso8601Custom = custom { 23 | var container = $1.singleValueContainer() 24 | let date = $0.addingTimeInterval(32400) 25 | try container.encode(Formatter.iso8601Custom.string(from: date)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Extensions/View+ColorScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ColorScheme.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | var previewInAllColorSchemes: some View { 12 | ForEach(ColorScheme.allCases, id: \.self, content: preferredColorScheme) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Extensions/View+dismissKeyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+dismissKeyboard.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func dismissKeyboard() { 12 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), 13 | to: nil, 14 | from: nil, 15 | for: nil 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/Albums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Albums.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Albums: Decodable { 11 | let data: [Album] 12 | } 13 | 14 | struct Album: Decodable { 15 | struct Artist: Decodable { 16 | let id: Int 17 | let name: String 18 | } 19 | 20 | let id: Int 21 | let title: String 22 | let description: String 23 | let releaseDate: String 24 | let artist: Artist 25 | let imageUrl: String 26 | let tracks: [TrackInfo]? 27 | } 28 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/Artist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Artist.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/09. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Artists: Decodable { 11 | let data: [Artist] 12 | } 13 | 14 | struct Artist: Decodable { 15 | let id: Int 16 | let name: String 17 | let imageUrl: String 18 | let genre: Genre 19 | } 20 | 21 | struct ArtistInfo: Decodable { 22 | struct Album: Decodable { 23 | let id: Int 24 | let title: String 25 | let imageUrl: String 26 | } 27 | 28 | let id: Int 29 | let name: String 30 | let imageUrl: String 31 | let genre: Genre 32 | let tracks: [TrackInfo] 33 | let albums: [Album] 34 | } 35 | 36 | struct Genre: Decodable { 37 | let name: String 38 | } 39 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/Magazines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Magazines.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Magazines: Decodable { 11 | let data: [Magazine] 12 | } 13 | 14 | struct Magazine: Decodable { 15 | let id: Int 16 | let title: String 17 | let imageUrl: String 18 | let date: String 19 | let category: String 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/Mixtape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mixtape.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Mixtapes: Decodable { 11 | let data: [Mixtape] 12 | } 13 | 14 | struct Mixtape: Decodable { 15 | let id: Int 16 | let title: String 17 | let subTitle: String 18 | let imageUrl: String 19 | } 20 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/News.swift: -------------------------------------------------------------------------------- 1 | // 2 | // News.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | struct NewsList: Decodable { 11 | let data: [News] 12 | } 13 | 14 | struct News: Decodable { 15 | let id: Int 16 | let title: String 17 | let imageUrl: String 18 | let date: String 19 | let link: String 20 | let albumId: Int 21 | } 22 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/Playlists.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Playlists.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Playlists: Decodable { 11 | let data: [Playlist] 12 | } 13 | 14 | struct Playlist: Decodable { 15 | let id: Int 16 | let title: String 17 | let subTitle: String? 18 | let description: String? 19 | let imageUrl: String 20 | let customized: Bool? 21 | let tracks: [TrackInfo]? 22 | } 23 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/UseCases/ChartsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartsUseCase.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/16. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | protocol ChartsUseCaseType { 12 | func loadTracks() -> AnyPublisher<[TrackInfo], UseCaseError> 13 | } 14 | 15 | struct ChartsUseCase: ChartsUseCaseType { 16 | 17 | private let network: NetworkServiceType 18 | 19 | init(network: NetworkServiceType = NetworkService()) { 20 | self.network = network 21 | } 22 | 23 | func loadTracks() -> AnyPublisher<[TrackInfo], UseCaseError> { 24 | return network.request(url: EndPoint.tracks.urlString, request: .get, body: nil) 25 | .decode(type: TrackResponse.self, decoder: JSONDecoder()) 26 | .mapError { error -> UseCaseError in 27 | switch error { 28 | case is NetworkError: 29 | return .networkError 30 | default: 31 | return .decodingError 32 | } 33 | } 34 | .map(\.data) 35 | .receive(on: DispatchQueue.main) 36 | .eraseToAnyPublisher() 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/UseCases/LibraryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryUseCase.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/17. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | protocol LibraryUseCaseType { 12 | func loadLikedTracks() -> AnyPublisher<[TrackInfo], UseCaseError> 13 | } 14 | 15 | struct LibraryUseCase: LibraryUseCaseType { 16 | 17 | private let network: NetworkServiceType 18 | 19 | init(network: NetworkServiceType = NetworkService()) { 20 | self.network = network 21 | } 22 | 23 | func loadLikedTracks() -> AnyPublisher<[TrackInfo], UseCaseError> { 24 | return network.request(url: EndPoint.libraryTracks.urlString, request: .get, body: nil) 25 | .decode(type: TrackResponse.self, decoder: JSONDecoder()) 26 | .mapError { error -> UseCaseError in 27 | switch error { 28 | case is NetworkError: 29 | return .networkError 30 | default: 31 | return .decodingError 32 | } 33 | } 34 | .map(\.data) 35 | .receive(on: DispatchQueue.main) 36 | .eraseToAnyPublisher() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/UseCases/SearchUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchUseCase.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/07. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | protocol SearchUseCaseType { 12 | func loadNews() -> AnyPublisher<[News], UseCaseError> 13 | } 14 | 15 | struct SearchUseCase: SearchUseCaseType { 16 | private let network: NetworkServiceType 17 | 18 | init(network: NetworkServiceType = NetworkService()) { 19 | self.network = network 20 | } 21 | 22 | func loadNews() -> AnyPublisher<[News], UseCaseError> { 23 | return network.request(url: EndPoint.newsList.urlString, request: .get, body: nil) 24 | .decode(type: NewsList.self, decoder: JSONDecoder()) 25 | .mapError { error -> UseCaseError in 26 | switch error { 27 | case is NetworkError: 28 | return .networkError 29 | default: 30 | return .decodingError 31 | } 32 | } 33 | .map(\.data) 34 | .receive(on: DispatchQueue.main) 35 | .eraseToAnyPublisher() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Models/UseCases/UseCaseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UseCaseError.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/10. 6 | // 7 | 8 | enum UseCaseError: Error { 9 | case networkError 10 | case decodingError 11 | case encodingError 12 | case unknownError 13 | } 14 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/ViewModels/ArtistSectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArtistSectionViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/09. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | final class ArtistSectionViewModel: ObservableObject { 12 | enum Input { 13 | case appear 14 | } 15 | 16 | struct State { 17 | var artists = [Artist]() 18 | } 19 | private let useCase: ArtistUseCaseType 20 | private var cancellables = Set() 21 | 22 | @Published private(set) var state = State() 23 | 24 | init(useCase: ArtistUseCaseType = ArtistUseCase()) { 25 | self.useCase = useCase 26 | } 27 | 28 | private func load() { 29 | useCase.loadArtists() 30 | .sink { _ in 31 | 32 | } receiveValue: { [weak self] artists in 33 | self?.state.artists = artists 34 | } 35 | .store(in: &cancellables) 36 | } 37 | 38 | func send(_ input: Input) { 39 | switch input { 40 | case .appear: 41 | load() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/ViewModels/MagazineViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagazineViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class MagazineViewModel: ObservableObject { 12 | private let useCase = MagazineUseCase() 13 | private var cancellables = Set() 14 | 15 | @Published var magazine: MagazineInfo? 16 | 17 | func load(using id: Int) { 18 | useCase.loadMagazine(using: id) 19 | .sink { _ in 20 | // error 처리 21 | } receiveValue: { [weak self] magazine in 22 | self?.magazine = magazine 23 | } 24 | .store(in: &cancellables) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/ViewModels/MainTabViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainTabViewModel.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/12. 6 | // 7 | 8 | import Combine 9 | import EventLogKit 10 | 11 | final class MainTabViewModel: ObservableObject { 12 | 13 | struct State { 14 | var tabViewSelection: ViewIdentifier = .today 15 | } 16 | 17 | private let eventLogger: EventLoggerType 18 | private var cancellables: Set = [] 19 | @Published var state = State() 20 | 21 | init(eventLogger: EventLoggerType = MiniVibeApp.eventLogger) { 22 | self.eventLogger = eventLogger 23 | 24 | $state 25 | .removeDuplicates { (prev, current) -> Bool in 26 | prev.tabViewSelection == current.tabViewSelection 27 | } 28 | .filter { $0.tabViewSelection != .none } 29 | .sink { tabItem in 30 | eventLogger.send(TabViewTransition(userId: 0, 31 | componentId: tabItem.tabViewSelection.description, 32 | page: tabItem.tabViewSelection.description)) 33 | } 34 | .store(in: &cancellables) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Artist/Components/ArtistItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArtistItem.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct ArtistItem: View { 12 | let artist: Artist 13 | 14 | var body: some View { 15 | VStack { 16 | KFImage(URL(string: artist.imageUrl)) 17 | .placeholder { 18 | Image("placeholder") 19 | .resizable() 20 | } 21 | .resizable() 22 | .aspectRatio(1, contentMode: .fit) 23 | .clipShape(Circle()) 24 | 25 | Text(artist.name) 26 | .font(.system(size: 17)) 27 | .foregroundColor(.primary) 28 | .lineLimit(2) 29 | 30 | Text("♥︎ 999") 31 | .font(.system(size: 12)) 32 | .foregroundColor(.secondary) 33 | } 34 | .frame(width: 100) 35 | } 36 | } 37 | 38 | struct ArtistItem_Previews: PreviewProvider { 39 | static var previews: some View { 40 | ArtistItem(artist: .init(id: 0, name: "", imageUrl: "", genre: .init(name: ""))) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Common/Menu/Components/MenuAlbumImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuAlbumImage.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/13. 6 | // 7 | 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct MenuAlbumImage: View { 12 | let imageUrl: String 13 | 14 | var body: some View { 15 | KFImage(URL(string: imageUrl)) 16 | .placeholder { 17 | Image("placeholder") 18 | .resizable() 19 | } 20 | .resizable() 21 | .aspectRatio(1, contentMode: .fit) 22 | .frame(width: 80) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Common/Menu/MenuCloseButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuCloseButton.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MenuCloseButton: View { 11 | init(action: @escaping () -> Void) { 12 | self.action = action 13 | } 14 | 15 | let action: () -> Void 16 | 17 | var body: some View { 18 | VStack(spacing: 0) { 19 | Divider() 20 | 21 | Button { 22 | action() 23 | } label: { 24 | Spacer() 25 | 26 | Text("닫기") 27 | .foregroundColor(.secondary) 28 | .font(.system(size: 18)) 29 | 30 | Spacer() 31 | } 32 | .padding(.vertical) 33 | } 34 | } 35 | } 36 | 37 | struct MenuCloseButton_Previews: PreviewProvider { 38 | static var previews: some View { 39 | MenuCloseButton(action: { }) 40 | .previewLayout(.fixed(width: 375, height: 80)) 41 | .previewInAllColorSchemes 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Common/Menu/MenuThumbnailButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuThumbnailButton.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/02. 6 | // 7 | 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct MenuThumbnailButton: View { 12 | let imageUrl: String 13 | let title: String 14 | let subtitle: String 15 | 16 | var body: some View { 17 | HStack { 18 | MenuAlbumImage(imageUrl: imageUrl) 19 | 20 | VStack(alignment: .leading, spacing: 4) { 21 | Text(title) 22 | .font(.system(size: 18, weight: .bold)) 23 | .foregroundColor(.primary) 24 | 25 | Text(subtitle) 26 | .foregroundColor(.secondary) 27 | } 28 | .lineLimit(1) 29 | .padding(.horizontal, 8) 30 | 31 | Spacer() 32 | } 33 | .padding(.horizontal) 34 | } 35 | } 36 | 37 | struct MenuThumbnailButton_Previews: PreviewProvider { 38 | static var previews: some View { 39 | MenuThumbnailButton(imageUrl: "", title: "bbb", subtitle: "ccc") 40 | .previewLayout(.fixed(width: 375, height: 80)) 41 | .previewInAllColorSchemes 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Common/MultiselectTabBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiselectTabBar.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MultiselectTabBar: View { 11 | @EnvironmentObject private var nowPlaying: NowPlayingViewModel 12 | 13 | var body: some View { 14 | ZStack { 15 | Rectangle() 16 | .foregroundColor(.accentColor) 17 | 18 | HStack { 19 | TabbarButton(type: .addToPlaylist) { 20 | 21 | } 22 | 23 | Spacer() 24 | 25 | TabbarButton(type: .save) { 26 | 27 | } 28 | 29 | Spacer() 30 | 31 | TabbarButton(type: .delete) { 32 | nowPlaying.send(.deleteSelectedTracks) 33 | } 34 | } 35 | .foregroundColor(.white) 36 | .padding(.horizontal, 50) 37 | } 38 | } 39 | } 40 | 41 | struct MultiselectTabBar_Previews: PreviewProvider { 42 | static var previews: some View { 43 | MultiselectTabBar() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Common/ThumbnailGridView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbnailGridView.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ThumbnailGridView: View { 11 | let title: String 12 | let album: [Album] 13 | 14 | var body: some View { 15 | ThumbnailGrid(albums: album) 16 | .navigationTitle(title) 17 | .navigationBarTitleDisplayMode(.inline) 18 | } 19 | } 20 | 21 | struct ThumbnailGridView_Previews: PreviewProvider { 22 | static var previews: some View { 23 | NavigationView { 24 | ThumbnailGridView(title: "좋아할 최신 앨범", album: []) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Common/TrackRows/Components/TrackRowImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrackRowImage.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/13. 6 | // 7 | 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct TrackRowImage: View { 12 | let imageUrl: String 13 | 14 | var body: some View { 15 | KFImage(URL(string: imageUrl)) 16 | .placeholder { 17 | Image("placeholder") 18 | .resizable() 19 | } 20 | .resizable() 21 | .frame(width: 50, height: 50) 22 | .border(Color.gray, width: 0.7) 23 | } 24 | } 25 | 26 | struct TrackRowMenu: View { 27 | var body: some View { 28 | Image(systemName: "ellipsis") 29 | .foregroundColor(.primary) 30 | .padding() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Library/LibraryArtistRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryArtistRow.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LibraryArtistRow: View { 11 | let artist: String 12 | 13 | var body: some View { 14 | HStack { 15 | Image("artist") 16 | .resizable() 17 | .aspectRatio(1, contentMode: .fit) 18 | .frame(width: 60) 19 | .clipShape(Circle()) 20 | 21 | Text(artist) 22 | .foregroundColor(.primary) 23 | 24 | Spacer() 25 | } 26 | } 27 | } 28 | 29 | struct LibraryArtistRow_Previews: PreviewProvider { 30 | static var previews: some View { 31 | LibraryArtistRow(artist: "방탄소년단") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Library/LibraryPlayListRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryPlayListRow.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LibraryPlayListRow: View { 11 | let title: String 12 | 13 | var body: some View { 14 | HStack(spacing: 24) { 15 | Image("album") 16 | .resizable() 17 | .aspectRatio(1, contentMode: .fit) 18 | .frame(width: 80) 19 | .overlay( 20 | Rectangle() 21 | .stroke(Color.secondary, lineWidth: 0.5) 22 | ) 23 | 24 | VStack(alignment: .leading) { 25 | Text(title) 26 | .foregroundColor(.primary) 27 | .font(.system(size: 17)) 28 | 29 | Text("10곡") 30 | .foregroundColor(.secondary) 31 | .font(.system(size: 14)) 32 | } 33 | 34 | Spacer() 35 | } 36 | } 37 | } 38 | 39 | struct LibraryPlayListRow_Previews: PreviewProvider { 40 | static var previews: some View { 41 | LibraryPlayListRow(title: "플레이리스트 이름") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Player/Components/PlayerHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerHeader.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PlayerHeader: View { 11 | let title: String 12 | @EnvironmentObject private var nowPlaying: NowPlayingViewModel 13 | 14 | var body: some View { 15 | HStack { 16 | Button { 17 | 18 | } label: { 19 | Image(systemName: "slider.horizontal.3") 20 | } 21 | 22 | Spacer() 23 | 24 | Text(title) 25 | 26 | Spacer() 27 | 28 | Button { 29 | nowPlaying.send(.togglePlayer) 30 | } label: { 31 | Image(systemName: "chevron.compact.down") 32 | } 33 | .padding(.vertical) 34 | } 35 | .foregroundColor(.primary) 36 | .padding(8) 37 | } 38 | } 39 | 40 | struct PlayerHeader_Previews: PreviewProvider { 41 | static var previews: some View { 42 | PlayerHeader(title: "오늘 Top 100") 43 | .previewLayout(.fixed(width: 350, height: 100)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Profile/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/12/02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileView: View { 11 | var body: some View { 12 | Text("Profile") 13 | } 14 | } 15 | 16 | struct ProfileView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | ProfileView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Search/NewsSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsSection.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/12/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewsSection: View { 11 | let width: CGFloat 12 | let newsList: [News] 13 | 14 | var body: some View { 15 | ScrollView(.horizontal, showsIndicators: false) { 16 | HStack { 17 | ForEach(newsList, id: \.id) { news in 18 | NewsItem(width: width, news: news) 19 | .frame(width: width * .sectionRatio, height: 250) 20 | } 21 | } 22 | .padding(.horizontal, width * .paddingRatio) 23 | } 24 | } 25 | } 26 | 27 | struct NewsSection_Previews: PreviewProvider { 28 | static var previews: some View { 29 | GeometryReader { geometry in 30 | NewsSection(width: geometry.size.width, 31 | newsList: []) 32 | 33 | } 34 | .previewLayout(.fixed(width: 375, height: 250)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Today/Components/RecommandedPlayListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecommandedPlayListItem.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RecommandedPlayListItem: View { 11 | var body: some View { 12 | VStack(alignment: .leading, spacing: 4) { 13 | Image("recommandedPlayListThumbnail") 14 | .resizable() 15 | .aspectRatio(1, contentMode: .fit) 16 | 17 | Text("Work/Study Lo-fi") 18 | .font(.system(size: 17)) 19 | 20 | Text("VIBE") 21 | .font(.system(size: 12)) 22 | .foregroundColor(.secondary) 23 | 24 | Text("집중력이 필요한 시간에 듣기 좋은 차분한 멜로디와 간질간질한 질감의 로파이 비트.") 25 | .font(.system(size: 12)) 26 | .foregroundColor(.secondary) 27 | } 28 | } 29 | } 30 | 31 | struct RecommandedPlayListItem_Previews: PreviewProvider { 32 | static var previews: some View { 33 | RecommandedPlayListItem() 34 | .previewLayout(.fixed(width: 375, height: 500)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Today/Components/StationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationItem.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/11/24. 6 | // 7 | 8 | import SwiftUI 9 | import KingfisherSwiftUI 10 | 11 | struct StationItem: View { 12 | let imageUrl: String 13 | 14 | var body: some View { 15 | ZStack(alignment: .bottomTrailing) { 16 | KFImage(URL(string: imageUrl)) 17 | .resizable() 18 | .aspectRatio(contentMode: .fit) 19 | 20 | Image(systemName: "play.circle.fill") 21 | .font(.system(size: 20)) 22 | .foregroundColor(Color.white.opacity(0.8)) 23 | .padding(10) 24 | } 25 | } 26 | } 27 | 28 | // swiftlint:disable line_length 29 | struct StationItem_Previews: PreviewProvider { 30 | static var previews: some View { 31 | StationItem(imageUrl: "https://music-phinf.pstatic.net/20181204_203/1543918901105PmOS5_PNG/mood_11_Party.png?type=f360") 32 | .previewLayout(.fixed(width: 375, height: 375)) 33 | } 34 | } 35 | // swiftlint:enable line_length 36 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Today/PreviewSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewSection.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PreviewSection: View { 11 | let width: CGFloat 12 | var body: some View { 13 | ScrollView(.horizontal, showsIndicators: false) { 14 | HStack { 15 | MockPreviewItem.item0 16 | .frame(width: width * .sectionRatio) 17 | 18 | MockPreviewItem.item1 19 | .frame(width: width * .sectionRatio) 20 | 21 | MockPreviewItem.item2 22 | .frame(width: width * .sectionRatio) 23 | 24 | } 25 | .padding(.horizontal, width * .paddingRatio) 26 | } 27 | } 28 | } 29 | 30 | struct PreviewSection_Previews: PreviewProvider { 31 | static var previews: some View { 32 | PreviewSection(width: 375) 33 | .previewLayout(.fixed(width: 375, height: 250)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Today/StationSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationSection.swift 3 | // MiniVibe 4 | // 5 | // Created by Sue Cho on 2020/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StationSection: View { 11 | let width: CGFloat 12 | let title: String 13 | 14 | var body: some View { 15 | VStack { 16 | SectionTitle(width: width, title: title) { 17 | StationList() 18 | .logTransition(identifier: .station, 19 | componentId: .sectionTitle(category: title)) 20 | } 21 | 22 | StationStack(width: width) 23 | } 24 | } 25 | } 26 | 27 | struct StationSection_Previews: PreviewProvider { 28 | static var previews: some View { 29 | StationSection(width: 375, title: "스테이션") 30 | .previewLayout(.fixed(width: 375, height: 200)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Today/StationStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationStack.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StationStack: View { 11 | let width: CGFloat 12 | let items: [Station] = MockStationItem.stations 13 | var body: some View { 14 | ScrollView(.horizontal, showsIndicators: false) { 15 | HStack(spacing: width * .spacingRatio) { 16 | ForEach(items, id: \.self) { item in 17 | StationItem(imageUrl: item.imageUrl) 18 | .frame(width: width * .thumbnailRatio) 19 | } 20 | } 21 | .padding(.horizontal, width * .paddingRatio) 22 | } 23 | } 24 | } 25 | 26 | struct StationStack_Previews: PreviewProvider { 27 | static var previews: some View { 28 | StationStack(width: 375) 29 | .previewLayout(.fixed(width: 375, height: 200)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibe/Views/Today/TodayTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayTitle.swift 3 | // MiniVibe 4 | // 5 | // Created by TTOzzi on 2020/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TodayTitle: View { 11 | let width: CGFloat 12 | 13 | var body: some View { 14 | HStack { 15 | Text("#내돈내듣 VIBE") 16 | .foregroundColor(.primary) 17 | .font(.title) 18 | .fontWeight(.heavy) 19 | .logSubscription(componentId: "mainSubscribe") 20 | 21 | Spacer() 22 | 23 | Image(systemName: "person") 24 | .clipShape(Circle()) 25 | .font(.system(size: 24)) 26 | .frame(width: 60, height: 60) 27 | 28 | } 29 | } 30 | } 31 | 32 | struct TodayTitle_Previews: PreviewProvider { 33 | static var previews: some View { 34 | GeometryReader { geometry in 35 | TodayTitle(width: geometry.size.width) 36 | .previewLayout(.fixed(width: 375, height: 100)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Extensions/AlbumViewModelExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumViewModelExtensions.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/14. 6 | // 7 | 8 | import Foundation 9 | @testable import MiniVibe 10 | 11 | extension AlbumViewModel.State.ActiveSheet: Equatable { 12 | public static func == (lhs: Self, rhs: Self) -> Bool { 13 | switch (lhs, rhs) { 14 | case (.album, .album): 15 | return true 16 | case let (.track(lhsInfo), .track(rhsInfo)): 17 | return lhsInfo === rhsInfo 18 | default: 19 | return false 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Extensions/NetworkError+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkError+Equatable.swift 3 | // MiniVibeTests 4 | // 5 | // Created by Sue Cho on 2020/11/30. 6 | // 7 | 8 | import Foundation 9 | @testable import MiniVibe 10 | 11 | extension NetworkError: Equatable { 12 | public static func == (lhs: Self, rhs: Self) -> Bool { 13 | switch (lhs, rhs) { 14 | case (.invalidURL, .invalidURL): return true 15 | case (.unsuccessfulResponse, .unsuccessfulResponse): return true 16 | case (.unknownError, .unknownError): return true 17 | default: return false 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Extensions/PlaylistViewModelExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaylistViewModelExtensions.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/15. 6 | // 7 | 8 | import Foundation 9 | @testable import MiniVibe 10 | 11 | extension PlaylistViewModel.State.ActiveSheet: Equatable { 12 | public static func == (lhs: Self, rhs: Self) -> Bool { 13 | switch (lhs, rhs) { 14 | case (.playlist, .playlist): 15 | return true 16 | case let (.track(lhsInfo), .track(rhsInfo)): 17 | return lhsInfo === rhsInfo 18 | default: 19 | return false 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/LibraryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LibraryUseCase.swift 3 | // MiniVibeTests 4 | // 5 | // Created by Sue Cho on 2020/12/17. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import MiniVibe 11 | 12 | struct MockLibraryUseCase: LibraryUseCaseType { 13 | let tracks: [TrackInfo] 14 | 15 | func loadLikedTracks() -> AnyPublisher<[TrackInfo], UseCaseError> { 16 | Just(tracks) 17 | .setFailureType(to: UseCaseError.self) 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockAlbumUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockAlbumUseCase.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/14. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | @testable import MiniVibe 11 | 12 | struct MockAlbumUseCase: AlbumUseCaseType { 13 | let album: Album 14 | 15 | func loadAlbum(with id: Int) -> AnyPublisher { 16 | return Just(album) 17 | .setFailureType(to: UseCaseError.self) 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockArtistSectionUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockArtistUseCase.swift 3 | // MiniVibeTests 4 | // 5 | // Created by Sue Cho on 2020/12/14. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import MiniVibe 11 | 12 | struct MockArtistSectionUseCase: ArtistUseCaseType { 13 | let artists: [Artist] 14 | let artistInfo: ArtistInfo 15 | 16 | func loadArtists() -> AnyPublisher<[Artist], UseCaseError> { 17 | return Just(artists) 18 | .setFailureType(to: UseCaseError.self) 19 | .eraseToAnyPublisher() 20 | } 21 | 22 | func loadArtist(with id: Int) -> AnyPublisher { 23 | return Just(artistInfo) 24 | .setFailureType(to: UseCaseError.self) 25 | .eraseToAnyPublisher() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockChartsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockChartsUseCase.swift 3 | // MiniVibeTests 4 | // 5 | // Created by Sue Cho on 2020/12/16. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import MiniVibe 11 | 12 | struct MockChartsUseCase: ChartsUseCaseType { 13 | let tracks: [TrackInfo] 14 | 15 | func loadTracks() -> AnyPublisher<[TrackInfo], UseCaseError> { 16 | return Just(tracks) 17 | .setFailureType(to: UseCaseError.self) 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockEventLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockEventLogger.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/14. 6 | // 7 | 8 | import Foundation 9 | @testable import EventLogKit 10 | 11 | struct MockEventLogger: EventLoggerType { 12 | 13 | var handler: ((EventLogType) -> Void)? 14 | 15 | func send(_ event: T) where T: EventLogType { 16 | handler?(event) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockNetworkServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNetworkServices.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/14. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | @testable import MiniVibe 11 | 12 | struct MockSuccessNetworkService: NetworkServiceType { 13 | let data: Data 14 | 15 | func request(url: String, request type: RequestType, body: Data?) -> AnyPublisher { 16 | return Just(data) 17 | .setFailureType(to: NetworkError.self) 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | 22 | struct MockFailureNetworkService: NetworkServiceType { 23 | func request(url: String, request type: RequestType, body: Data?) -> AnyPublisher { 24 | return Just(Data()) 25 | .setFailureType(to: NetworkError.self) 26 | .eraseToAnyPublisher() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockPlayerDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPlayerDataManager.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/16. 6 | // 7 | 8 | import Foundation 9 | @testable import MiniVibe 10 | 11 | struct MockPlayerDataManager: PlayerDataManagerType { 12 | let data: [TrackViewModel] 13 | var saveHandler: (([TrackViewModel]) -> Void)? 14 | 15 | func fetch() -> [TrackViewModel] { 16 | return data 17 | } 18 | 19 | func saveTracks(tracks: [TrackViewModel]) { 20 | saveHandler?(tracks) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockSearchUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSearchUseCase.swift 3 | // MiniVibeTests 4 | // 5 | // Created by TTOzzi on 2020/12/15. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | @testable import MiniVibe 11 | 12 | struct MockSearchUseCase: SearchUseCaseType { 13 | let news: [News] 14 | 15 | func loadNews() -> AnyPublisher<[News], UseCaseError> { 16 | return Just(news) 17 | .setFailureType(to: UseCaseError.self) 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MiniVibe/MiniVibeTests/Mock/MockTrackUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockTrackUseCase.swift 3 | // MiniVibeTests 4 | // 5 | // Created by Sue Cho on 2020/12/14. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import MiniVibe 11 | 12 | struct MockTrackUseCase: TrackUseCaseType { 13 | let track: TrackInfo 14 | let success = true 15 | 16 | func loadTrack(id: Int) -> AnyPublisher { 17 | Just(track) 18 | .setFailureType(to: UseCaseError.self) 19 | .eraseToAnyPublisher() 20 | } 21 | 22 | func likeTrack(id: Int) -> AnyPublisher { 23 | Just(success) 24 | .setFailureType(to: UseCaseError.self) 25 | .eraseToAnyPublisher() 26 | } 27 | 28 | func cancelLikedTrack(id: Int) -> AnyPublisher { 29 | Just(success) 30 | .setFailureType(to: UseCaseError.self) 31 | .eraseToAnyPublisher() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MiniVibe/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'MiniVibe' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for MiniVibe 9 | pod 'SwiftLint' 10 | end 11 | -------------------------------------------------------------------------------- /MiniVibe/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftLint (0.41.0) 3 | 4 | DEPENDENCIES: 5 | - SwiftLint 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - SwiftLint 10 | 11 | SPEC CHECKSUMS: 12 | SwiftLint: c585ebd615d9520d7fbdbe151f527977b0534f1e 13 | 14 | PODFILE CHECKSUM: 4e1ab59ce9d05b1ab806de7beebf195df48f8ceb 15 | 16 | COCOAPODS: 1.9.3 17 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | [ 5 | "styled-components", 6 | { 7 | "ssr":true, 8 | "displayName": true, 9 | "preprocess": false 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "semi": true, 12 | "endOfLine": "auto" 13 | } -------------------------------------------------------------------------------- /client/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin') 2 | 3 | module.exports = { 4 | "stories": [ 5 | "../**/*.stories.mdx", 6 | "../**/*.stories.@(js|jsx|ts|tsx)" 7 | ], 8 | "addons": [ 9 | "@storybook/addon-links", 10 | "@storybook/addon-essentials" 11 | ], 12 | "webpackFinal": async (config) => { 13 | config.resolve.plugins.push(new TsconfigPathsPlugin({})); 14 | return config; 15 | }, 16 | } -------------------------------------------------------------------------------- /client/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { withNextRouter } from 'storybook-addon-next-router'; 2 | import { addDecorator } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { Provider } from 'react-redux'; 6 | 7 | const store = { 8 | getState: () => { 9 | return { 10 | user: { id: 1 }, 11 | musicPlayer: { 12 | nowPlaying: { id: 1 }, 13 | playTime: 120, 14 | }, 15 | }; 16 | }, 17 | subscribe: () => 0, 18 | dispatch: action('dispatch'), 19 | }; 20 | 21 | const ProviderWrapper = ({ children, store }) => {children}; 22 | 23 | export const withProvider = (story) => {story()}; 24 | 25 | addDecorator(withNextRouter({})); 26 | 27 | addDecorator(withProvider); 28 | 29 | export const parameters = { 30 | actions: { argTypesRegex: '^on[A-Z].*' }, 31 | }; 32 | -------------------------------------------------------------------------------- /client/assets/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | ul, 4 | li { 5 | padding: 0; 6 | margin: 0; 7 | } 8 | 9 | body { 10 | /* background-color: #fbfbfb; */ 11 | background-color: black; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | a { 19 | text-decoration: none; 20 | } 21 | 22 | button { 23 | border: none; 24 | background-color: inherit; 25 | } 26 | -------------------------------------------------------------------------------- /client/components/atoms/A/A.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import A from './A'; 3 | 4 | export default { 5 | title: 'Atoms/A', 6 | component: A, 7 | }; 8 | 9 | const STORY_HREF = 'localhost:3000'; 10 | const STORY_TEXT = '테스트 메세지 입니다. TEST MESSAGE'; 11 | 12 | export const Default = () => {STORY_TEXT}; 13 | 14 | export const Primary = () => ( 15 | 16 | {STORY_TEXT} 17 | 18 | ); 19 | 20 | export const Secondary = () => ( 21 | 22 | {STORY_TEXT} 23 | 24 | ); 25 | 26 | export const Tertiary = () => ( 27 | 28 | {STORY_TEXT} 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /client/components/atoms/A/A.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import Link from 'next/link'; 3 | import StyledA from './A.styles'; 4 | import { useSelector } from 'react-redux'; 5 | import useClickEventLog from '@hooks/useClickEventLog'; 6 | 7 | interface AProps { 8 | children: ReactNode; 9 | href?: string; 10 | variant?: 'primary' | 'secondary' | 'tertiary'; 11 | } 12 | 13 | const A = ({ children, href, variant }: AProps) => { 14 | const user = useSelector((state) => state.user); 15 | const handleClick = useClickEventLog({ userId: user.id, href }); 16 | 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default A; 27 | -------------------------------------------------------------------------------- /client/components/atoms/A/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './A'; -------------------------------------------------------------------------------- /client/components/atoms/Button/AddButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent, useState, useContext } from 'react'; 2 | import styled from 'styled-components'; 3 | import CheckIcon from '@material-ui/icons/Check'; 4 | import AddIcon from '@material-ui/icons/Add'; 5 | 6 | interface AddButtonProps { 7 | onClick?: (e: MouseEvent) => void; 8 | variant?: 'primary'; 9 | liked: boolean; 10 | } 11 | 12 | const Container = styled.div` 13 | margin-top: 7px; 14 | margin-left: 10px; 15 | `; 16 | 17 | const AddButton = ({ onClick, variant, liked }: AddButtonProps) => { 18 | return ( 19 | 20 | {!liked && } 21 | {liked && } 22 | 23 | ); 24 | }; 25 | 26 | export default AddButton; 27 | -------------------------------------------------------------------------------- /client/components/atoms/Button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './Button'; 3 | 4 | export default { 5 | title: 'Atoms/Button', 6 | component: Button, 7 | }; 8 | 9 | const STORY_BUTTON_TEXT = 'BUTTON'; 10 | 11 | export const Default = () => ; 12 | 13 | export const Primary = () => ; 14 | 15 | export const Secondary = () => ; 16 | -------------------------------------------------------------------------------- /client/components/atoms/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, MouseEvent, ComponentType } from 'react'; 2 | import styled from 'styled-components'; 3 | import StyledButton from './Button.styles'; 4 | 5 | interface ButtonProps { 6 | children: ReactNode; 7 | icon?: ComponentType; 8 | onClick?: (e: MouseEvent) => void; 9 | variant?: 'primary' | 'secondary'; 10 | width?: string; 11 | height?: string; 12 | disabled?: boolean | undefined; 13 | htmlType? 14 | id? 15 | } 16 | 17 | const IconWrapper = styled.div` 18 | margin-right: 3px; 19 | `; 20 | 21 | const Button = ({ children, onClick, icon: Icon, variant, width, height, disabled }: ButtonProps) => ( 22 | 23 | {Icon && ( 24 | 25 | 26 | 27 | )} 28 | {children} 29 | 30 | ); 31 | 32 | export default Button; 33 | -------------------------------------------------------------------------------- /client/components/atoms/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Button'; 2 | -------------------------------------------------------------------------------- /client/components/atoms/CheckBox/CheckBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckBox from '.'; 3 | 4 | export default { 5 | title: 'Atoms/CheckBox', 6 | component: CheckBox, 7 | }; 8 | 9 | const STORY_ID = 1; 10 | const onChange = (e) => {}; 11 | export const Default = () => ; 12 | -------------------------------------------------------------------------------- /client/components/atoms/Circle/Circle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Circle from './Circle'; 3 | 4 | export default { 5 | title: 'Atoms/Circle', 6 | component: Circle, 7 | }; 8 | 9 | export const Default = () => ; 10 | 11 | export const Primary = () => ; 12 | -------------------------------------------------------------------------------- /client/components/atoms/Circle/Circle.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface StyledCircleProps { 4 | variant?: 'primary'; 5 | } 6 | 7 | const getCircleStyle = (props: StyledCircleProps) => { 8 | let diameter; let boxShadow; 9 | 10 | switch (props.variant) { 11 | case 'primary': 12 | diameter = '38px'; 13 | boxShadow = '0 1px 3px 0 rgba(0,0,0,.2)'; 14 | break; 15 | default: 16 | diameter = '42px'; 17 | boxShadow = 'none'; 18 | break; 19 | } 20 | 21 | return ` 22 | width: ${diameter}; 23 | height: ${diameter}; 24 | box-shadow: ${boxShadow}; 25 | `; 26 | }; 27 | 28 | export const StyledCircle = styled.div` 29 | ${(props) => getCircleStyle(props)}; 30 | cursor: pointer; 31 | border-radius: 70%; 32 | background-color: white; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | z-index: 3; 37 | `; 38 | 39 | export default StyledCircle; 40 | -------------------------------------------------------------------------------- /client/components/atoms/Circle/Circle.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, MouseEvent } from 'react'; 2 | import StyledCircle from './Circle.styles'; 3 | 4 | interface CircleProps { 5 | children?: ReactNode; 6 | variant?: 'primary'; 7 | onClick?: (e: MouseEvent) => void; 8 | } 9 | 10 | const Circle = ({ children, variant, onClick }: CircleProps) => ( 11 | 12 | {children} 13 | 14 | ); 15 | 16 | export default Circle; 17 | -------------------------------------------------------------------------------- /client/components/atoms/Heart/Heart.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Heart from './Heart'; 3 | 4 | export default { 5 | title: 'Atoms/Heart', 6 | component: Heart, 7 | }; 8 | 9 | export const Default = () => ; 10 | 11 | export const MusicPlayer = () => ; -------------------------------------------------------------------------------- /client/components/atoms/IconButton/IconButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from './IconButton'; 3 | import CloseIcon from '@material-ui/icons/Close'; 4 | import SearchIcon from '@material-ui/icons/Search'; 5 | 6 | export default { 7 | title: 'Atoms/IconButton', 8 | component: IconButton, 9 | }; 10 | 11 | const STORY_IconButton_TEXT = 'IconButton'; 12 | export const PlainGreyRegular = () => ; 13 | 14 | export const PlainGreySmall = () => ; 15 | 16 | export const PlainWhiteRegular = () => ; 17 | 18 | export const PlainBlackRegular = () => ; 19 | -------------------------------------------------------------------------------- /client/components/atoms/IconButton/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, MouseEvent, ComponentType } from 'react'; 2 | import styled from 'styled-components'; 3 | import StyledIconButton from './IconButton.styles'; 4 | 5 | interface IconButtonProps { 6 | icon: any; 7 | onClick?: (e: MouseEvent) => void; 8 | variant?: 'plainGreyRegular' | 'plainGreySmall' | 'plainWhiteRegular' | 'plainBlackRegular' ; 9 | } 10 | 11 | const IconWrapper = styled.div` 12 | margin-right: 10px; 13 | `; 14 | 15 | const IconButton = ({ 16 | onClick, icon: Icon, variant, 17 | }: IconButtonProps) => ( 18 | 19 | 20 | 21 | ); 22 | 23 | export default IconButton; 24 | -------------------------------------------------------------------------------- /client/components/atoms/IconButton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './IconButton'; 2 | -------------------------------------------------------------------------------- /client/components/atoms/Image/Image.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from './Image'; 3 | 4 | export default { 5 | title: 'Atoms/Image', 6 | component: Image, 7 | }; 8 | 9 | const STORY_IMAGE_SRC = 10 | 'https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg?type=ff300_300&v=20191231151906'; 11 | 12 | export const Default = () => ; 13 | 14 | export const Primary = () => ; 15 | 16 | export const NormalMagazine = () => ; 17 | 18 | export const News = () => ; 19 | 20 | export const TrackRowCard = () => ; 21 | 22 | export const TrackInfo = () => ; 23 | 24 | export const LyricTrackInfo = () => ; 25 | 26 | export const SmallArtistImage = () => ; 27 | 28 | export const RegularArtistImage = () => ; 29 | 30 | export const LargeArtistImage = () => ; 31 | -------------------------------------------------------------------------------- /client/components/atoms/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { func, node, string } from 'prop-types'; 3 | 4 | import StyledImage from './Image.styles'; 5 | 6 | const Image = ({ 7 | variant, src 8 | }) => ; 9 | 10 | // Expected prop values 11 | Image.propTypes = { 12 | variant: string, 13 | src: string 14 | }; 15 | 16 | // Default prop values 17 | Image.defaultProps = { 18 | children: 'Image', 19 | }; 20 | 21 | export default Image; 22 | -------------------------------------------------------------------------------- /client/components/atoms/Input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Input from './Input'; 3 | 4 | export default { 5 | title: 'Atoms/Input', 6 | component: Input, 7 | }; 8 | 9 | export const Default = () => ; 10 | 11 | export const Search = () => ; 12 | -------------------------------------------------------------------------------- /client/components/atoms/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { func, node, string } from 'prop-types'; 3 | 4 | import StyledInput from './Input.styles'; 5 | import { InputProps } from '@interfaces/props'; 6 | 7 | const Input = ({ variant, name, value, onChange }: InputProps) => { 8 | if (variant === 'search') 9 | return ( 10 | 19 | ); 20 | return ( 21 | 30 | ); 31 | }; 32 | 33 | // Expected prop values 34 | Input.propTypes = { 35 | onClick: func, 36 | variant: string, 37 | }; 38 | 39 | // Default prop values 40 | Input.defaultProps = { 41 | children: 'Input text', 42 | }; 43 | 44 | export default Input; 45 | -------------------------------------------------------------------------------- /client/components/atoms/Label/Label.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Label from './Label'; 3 | 4 | export default { 5 | title: 'Atoms/Label', 6 | component: Label, 7 | }; 8 | 9 | const STORY_CHILDREN = 'Label'; 10 | const STORY_SPECIAL_CHILDREN = 'Special'; 11 | 12 | export const Special = () => ( 13 | 16 | ); 17 | 18 | export const Primary = () => ( 19 | 22 | ); 23 | 24 | export const Secondary = () => ( 25 | 28 | ); 29 | 30 | export const Default = () => ; 31 | -------------------------------------------------------------------------------- /client/components/atoms/Label/Label.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, MouseEvent } from 'react'; 2 | import StyledSpan from './Label.styles'; 3 | 4 | interface LabelProps { 5 | children: ReactNode; 6 | onClick?: (e: MouseEvent) => void; 7 | variant?: 'special' | 'primary' | 'secondary'; 8 | selected?: boolean; 9 | } 10 | 11 | const Label = ({ 12 | children, onClick, variant, selected, 13 | }: LabelProps) => ( 14 | 15 | {children} 16 | 17 | ); 18 | 19 | export default Label; 20 | -------------------------------------------------------------------------------- /client/components/atoms/Label/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Label'; 2 | -------------------------------------------------------------------------------- /client/components/atoms/MenuLink/MenuLink.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuLink from './MenuLink'; 3 | 4 | export default { 5 | title: 'Atoms/MenuLink', 6 | component: MenuLink, 7 | }; 8 | 9 | const STORY_BUTTON_TEXT = '메뉴 이름'; 10 | const STORY_HREF = 'localhost:3000'; 11 | 12 | export const Default = () => {STORY_BUTTON_TEXT}; 13 | 14 | export const Selected = () => ( 15 | 16 | {STORY_BUTTON_TEXT} 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /client/components/atoms/MenuLink/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MenuLink'; 2 | -------------------------------------------------------------------------------- /client/components/atoms/NaverLoginButton/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import apiUrl from '@constants/apiUrl'; 3 | import useClickEventLog from '@hooks/useClickEventLog'; 4 | 5 | const NaverLogin = styled.a` 6 | display: block; 7 | margin-top: 40px; 8 | `; 9 | 10 | const NaverLoginButton = () => { 11 | const naverClick = useClickEventLog({ userId: null, href: `${apiUrl.login}/naver-login` }); 12 | 13 | return ( 14 | 15 | 네이버로 로그인 20 | 21 | ); 22 | }; 23 | 24 | export default NaverLoginButton; 25 | -------------------------------------------------------------------------------- /client/components/atoms/PlayTriangle/PlayTriangle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlayTriangle from './PlayTriangle'; 3 | 4 | export default { 5 | title: 'Atoms/PlayTriangle', 6 | component: PlayTriangle, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/atoms/PlayTriangle/PlayTriangle.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledPlayTriangle = styled.svg` 4 | z-index: 3; 5 | `; 6 | 7 | export const StyledPlayTriangleContainer = styled.div` 8 | z-index: 3; 9 | position: absolute; 10 | height: 23px; 11 | `; -------------------------------------------------------------------------------- /client/components/atoms/PlayTriangle/PlayTriangle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { StyledPlayTriangle, StyledPlayTriangleContainer } from './PlayTriangle.styles'; 4 | 5 | const PlayTriangle = () => { 6 | return( 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | }; 14 | 15 | export default PlayTriangle; 16 | -------------------------------------------------------------------------------- /client/components/atoms/Playtime/Playtime.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Playtime from './index'; 3 | 4 | export default { 5 | title: 'Atoms/Playtime', 6 | component: Playtime, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/atoms/Playtime/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, MouseEvent } from 'react'; 2 | import styled from 'styled-components'; 3 | import { convertToHHSS } from '@utils/time'; 4 | 5 | const Container = styled.div` 6 | font-size: 14px; 7 | line-height: 14px; 8 | text-align: right; 9 | `; 10 | 11 | const CurrentTime = styled.span` 12 | color: #747474; 13 | padding-right: 2px; 14 | `; 15 | 16 | const TotalTime = styled.span` 17 | color: #bcbcbc; 18 | `; 19 | 20 | interface PlaytimeProps { 21 | total: number; 22 | current: number; 23 | } 24 | 25 | const Playtime = ({ current, total }: PlaytimeProps) => { 26 | return( 27 | 28 | {convertToHHSS(current)} 29 | /{convertToHHSS(total)} 30 | 31 | )}; 32 | 33 | export default Playtime; 34 | -------------------------------------------------------------------------------- /client/components/atoms/Text/HiddenText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Span = styled.span` 5 | overflow: hidden; 6 | width: 1px; 7 | height: 1px; 8 | position: absolute; 9 | clip: reat(0, 0, 0, 0); 10 | `; 11 | const HiddenText = ({ children }) => {children}; 12 | 13 | export default HiddenText; 14 | -------------------------------------------------------------------------------- /client/components/atoms/Text/Text.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Text from '.'; 3 | 4 | export default { 5 | title: 'Atoms/Text', 6 | component: Text, 7 | }; 8 | 9 | const STORY_TEXT = '테스트 메세지 입니다. TEST MESSAGE'; 10 | 11 | export const Default = () => {STORY_TEXT}; 12 | 13 | export const Primary = () => {STORY_TEXT}; 14 | 15 | export const Secondary = () => {STORY_TEXT}; 16 | 17 | export const Tertiary = () => {STORY_TEXT}; 18 | 19 | export const PlainStrong = () => {STORY_TEXT}; 20 | -------------------------------------------------------------------------------- /client/components/atoms/Text/Text.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface StyledTextProps { 4 | variant?: 'primary' | 'secondary' | 'tertiary' | 'regularStrong'; 5 | } 6 | 7 | const getTextStyle = (props: StyledTextProps) => { 8 | let color; let fontSize = '15px'; let fontWeight = '400'; 9 | 10 | switch (props.variant) { 11 | case 'primary': 12 | color = '#999'; 13 | fontSize = '13px'; 14 | break; 15 | case 'secondary': 16 | color = 'white'; 17 | break; 18 | case 'tertiary': 19 | color = 'black'; 20 | fontWeight = '700'; 21 | fontSize = '30px'; 22 | break; 23 | case 'regularStrong': 24 | color = 'black'; 25 | fontWeight = '600'; 26 | fontSize = '16px'; 27 | break; 28 | default: 29 | color = 'black'; 30 | break; 31 | } 32 | 33 | return ` 34 | color: ${color}; 35 | font-size: ${fontSize}; 36 | font-weight: ${fontWeight} 37 | `; 38 | }; 39 | 40 | export const StyledText = styled.p` 41 | ${(props) => getTextStyle(props)}; 42 | margin: 0; 43 | `; 44 | 45 | export default StyledText; 46 | -------------------------------------------------------------------------------- /client/components/atoms/Text/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import StyledText from './Text.styles'; 3 | 4 | interface TextProps { 5 | children: ReactNode; 6 | variant?: 'primary' | 'secondary' | 'tertiary' | 'regularStrong'; 7 | } 8 | 9 | const Text = ({ children, variant }: TextProps) => ( 10 | 11 | {children} 12 | 13 | ); 14 | 15 | export default Text; 16 | -------------------------------------------------------------------------------- /client/components/molecules/ArtistLikeButton/ArtistLikeButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ArtistLikeButton from './ArtistLikeButton'; 3 | 4 | export default { 5 | title: 'Molecules/ArtistLikeButton', 6 | component: ArtistLikeButton, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/ArtistLikeButton/ArtistLikeButton.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledArtistLikeButton = styled.div` 4 | width: 38px; 5 | height: 38px; 6 | border-radius: 70%; 7 | background-color: white; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | box-shadow: 0 1px 3px 0 rgba(0,0,0,.2); 12 | z-index: 2; 13 | position: absolute; 14 | `; -------------------------------------------------------------------------------- /client/components/molecules/ArtistLikeButton/ArtistLikeButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledArtistLikeButton } from './ArtistLikeButton.styles'; 3 | import FavoriteIcon from '@material-ui/icons/Favorite'; 4 | import Circle from '../../atoms/Circle/Circle'; 5 | 6 | const ArtistLikeButton = () => { 7 | return( 8 | 9 | 10 | 11 | ) 12 | }; 13 | 14 | export default ArtistLikeButton; 15 | -------------------------------------------------------------------------------- /client/components/molecules/ArtistThumbnail/LibraryArtistThumbnail/LibraryArtistThumbnail.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LibraryArtistThumbnail from './LibraryArtistThumbnail'; 3 | import { LibraryArtistThumbnailProps } from '@interfaces/props'; 4 | 5 | export default { 6 | title: 'Molecules/LibraryArtistThumbnail', 7 | component: LibraryArtistThumbnail, 8 | }; 9 | 10 | const Artistdata = { 11 | id: 3, 12 | name: "이영지", 13 | imageUrl: "https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg", 14 | genre: { 15 | id: 1, 16 | name: "랩/힙합" 17 | } 18 | }; 19 | 20 | export const Default = () => ( 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /client/components/molecules/ArtistThumbnail/NormalArtistThumbnail/NormalArtistThumbnail.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NormalArtistThumbnail from './NormalArtistThumbnail'; 3 | import { NormalArtistThumbnailProps } from '@interfaces/props'; 4 | 5 | export default { 6 | title: 'Molecules/NormalArtistThumbnail', 7 | component: NormalArtistThumbnail, 8 | }; 9 | 10 | const Artistdata = { 11 | id: 3, 12 | name: "이영지", 13 | imageUrl: "https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg", 14 | genre: { 15 | id: 1, 16 | name: "랩/힙합" 17 | } 18 | }; 19 | 20 | export const Default = () => ( 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /client/components/molecules/ArtistThumbnail/NormalArtistThumbnail/NormalArtistThumbnail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Image from '../../../atoms/Image/Image'; 4 | 5 | import { NormalArtistThumbnailProps } from '@interfaces/props'; 6 | 7 | const Card = styled.div` 8 | width: 180px; 9 | height: 220px; 10 | position: relative; 11 | display: flex; 12 | justify-content:center; 13 | `; 14 | 15 | const ArtistName = styled.div` 16 | position: absolute; 17 | bottom: 15px; 18 | font-size: 14px; 19 | font-weight: 400; 20 | `; 21 | 22 | const NormalArtistThumbnail = ( data : NormalArtistThumbnailProps ) => { 23 | const { id, name, imageUrl } = data; 24 | 25 | return ( 26 | 27 | 28 | {name} 29 | 30 | ) 31 | }; 32 | 33 | export default NormalArtistThumbnail; -------------------------------------------------------------------------------- /client/components/molecules/ChartCard/ChartCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChartCard from './ChartCard'; 3 | import { ChartCardProps } from '@interfaces/props'; 4 | 5 | export default { 6 | title: 'Molecules/ChartCard', 7 | component: ChartCard, 8 | }; 9 | 10 | const item = 11 | { 12 | id: 5, 13 | rank: 1, 14 | title: '그냥', 15 | album: { 16 | id: 0, 17 | imageUrl: 'https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg?type=ff300_300&v=20191231151906', 18 | }, 19 | artist: { 20 | id: 0, 21 | name: '이영지', 22 | } 23 | } 24 | 25 | export const Default = () => ( 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /client/components/molecules/ChartCard/ChartCard.styles.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Play } from '@components/molecules/TrackPlayButton/TrackPlayButton.styles'; 4 | 5 | export const Card = styled.li` 6 | list-style: none; 7 | display: flex; 8 | padding: 8px 0; 9 | margin: 0; 10 | &:hover ${Play} { 11 | visibility: visible; 12 | opacity: 60%; 13 | } 14 | & + & { 15 | border-top: 1px solid #ececec; 16 | } 17 | `; 18 | 19 | export const Rank = styled.div` 20 | width: 25px; 21 | padding-left: 13px; 22 | text-align: center; 23 | `; 24 | export const SongInfo = styled.div` 25 | display: flex; 26 | flex-flow: column; 27 | padding: 0 20px 0 12px; 28 | line-height: 21px; 29 | `; 30 | -------------------------------------------------------------------------------- /client/components/molecules/ChartCard/ChartCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from '@components/atoms/Image/Image'; 3 | import Text from '@components/atoms/Text'; 4 | import TrackPlayButton from '@components/molecules/TrackPlayButton'; 5 | import { Card, Rank, SongInfo } from './ChartCard.styles'; 6 | import { ChartCardProps } from 'interfaces/props'; 7 | import A from '@components/atoms/A/A'; 8 | 9 | const ChartCard = ( data : ChartCardProps) => { 10 | 11 | const { id, rank, title, album, artist, playtime } = data; 12 | const { id: albumId, imageUrl } = album; 13 | const { id: artistId, name: artistName } = artist; 14 | 15 | return ( 16 | 17 | 18 | 19 | {rank.toString()} 20 | 21 | 22 | 23 | {title} 24 | 25 | 26 | {artistName} 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default ChartCard; 34 | -------------------------------------------------------------------------------- /client/components/molecules/ContentsThumbnail/ContentsThumbnail.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContentsThumbnail from './ContentsThumbnail'; 3 | 4 | export default { 5 | title: 'Molecules/ContentsThumbnail', 6 | component: ContentsThumbnail, 7 | }; 8 | 9 | const data = { 10 | id: 1, 11 | imageUrl: 'https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg?type=ff300_300&v=20191231151906' 12 | } 13 | 14 | export const News = () => ; 15 | 16 | export const MainMagazine = () => ; 17 | 18 | export const NormalMagazine = () => ; 19 | 20 | export const RecommendPlaylist = () => ; 21 | 22 | export const NormalPlaylist = () => ; 23 | -------------------------------------------------------------------------------- /client/components/molecules/DropdownMenu/DropdownMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { action } from '@storybook/addon-actions'; 3 | import DropdownMenu from './index'; 4 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 5 | 6 | export default { 7 | title: 'Molecules/DropdownMenu', 8 | component: DropdownMenu, 9 | }; 10 | 11 | const STORY_ID = 'test menu'; 12 | const STORY_CONTROL_COMPONENT = ArrowDropDownIcon; 13 | const STORY_MENU_ITEMS = [ 14 | { 15 | content: '좋아요', 16 | handleClick: action('clicked'), 17 | }, 18 | { 19 | content: 'MP3 구매', 20 | handleClick: action('clicked'), 21 | }, 22 | ]; 23 | 24 | export const Default = () => ( 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /client/components/molecules/GenreCard/GenreCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GenreCard from './GenreCard'; 3 | 4 | export default { 5 | title: 'Molecules/GenreCard', 6 | component: GenreCard, 7 | }; 8 | 9 | const id = 1; 10 | const name = "힙합"; 11 | 12 | export const Default = () => ; 13 | 14 | export const Selected = () => ; 15 | -------------------------------------------------------------------------------- /client/components/molecules/GenreCard/GenreCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Text from '../../atoms/Text'; 4 | import { getRandomColor } from '@utils/color'; 5 | 6 | const Card = styled.div` 7 | display: flex; 8 | align-items: center; 9 | height: 60px; 10 | padding: 0 9px; 11 | color: #232323; 12 | text-align: left; 13 | border-radius: 4px; 14 | background-color: #ececec; 15 | margin-bottom: 12px; 16 | `; 17 | 18 | const Bar = styled.div` 19 | width: 4px; 20 | height: 42px; 21 | border-radius: 3px; 22 | background-color: ${(props) => props.color}; 23 | `; 24 | 25 | const Content = styled.div` 26 | padding: 0 15px; 27 | `; 28 | 29 | interface GenreCardProps { 30 | id: number; 31 | name: string; 32 | href?: string; 33 | color?: string; 34 | } 35 | 36 | const GenreCard = ({ id, name, href, color }: GenreCardProps) => ( 37 | 38 | {/* TODO : Link 컴포넌트로 감싸기 */} 39 | {/* */} 40 | 41 | 42 | {name} 43 | 44 | 45 | ); 46 | 47 | export default GenreCard; 48 | -------------------------------------------------------------------------------- /client/components/molecules/GenreCard/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './GenreCard'; 2 | -------------------------------------------------------------------------------- /client/components/molecules/MainMenu/MainMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainMenu from './index'; 3 | 4 | export default { 5 | title: 'Molecules/MainMenu', 6 | component: MainMenu, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/MainMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import HomeIcon from '@material-ui/icons/Home'; 4 | import AlbumIcon from '@material-ui/icons/Album'; 5 | import LibraryMusicIcon from '@material-ui/icons/LibraryMusic'; 6 | import MenuBookIcon from '@material-ui/icons/MenuBook'; 7 | import InsertChartIcon from '@material-ui/icons/InsertChart'; 8 | import MenuLink from '@components/atoms/MenuLink'; 9 | 10 | const MenuWrapper = styled.div` 11 | & > a { 12 | margin-top: 8px; 13 | } 14 | `; 15 | 16 | const MainMenu = () => ( 17 | 18 | 19 | 투데이 20 | 21 | 22 | 차트 23 | 24 | 25 | DJ 스테이션 26 | 27 | 28 | ViBE MAG 29 | 30 | 31 | 이달의 노래 32 | 33 | 34 | ); 35 | 36 | export default MainMenu; 37 | -------------------------------------------------------------------------------- /client/components/molecules/NoDataContainer/NoDataContainer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NoDataContainer from '.'; 3 | 4 | export default { 5 | title: 'Molecules/NoDataContainer', 6 | component: NoDataContainer, 7 | }; 8 | 9 | export const Defatul = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/PlayButton/PlayButton.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledCircle = styled.div` 4 | width: 43px; 5 | height: 43px; 6 | border-radius: 70%; 7 | background-color: white; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | position: absolute; 12 | transition: 0.2s ease-in; 13 | opacity: 91%; 14 | `; 15 | 16 | export const StyledPlayButton = styled.div` 17 | bottom: 0; 18 | left: 2%; 19 | width: 53px; 20 | height: 53px; 21 | position: absolute; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | &:hover ${StyledCircle}{ 26 | width: 45px; 27 | height: 45px; 28 | opacity: 100%; 29 | transition: 0.2s ease-in; 30 | } 31 | `; 32 | 33 | export default StyledPlayButton; -------------------------------------------------------------------------------- /client/components/molecules/PlayControllerButtons/PlayControllerButtons.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlayControllerButtons from './index'; 3 | 4 | export default { 5 | title: 'Molecules/PlayControllerButtons', 6 | component: PlayControllerButtons, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/PlaylistDisplayButton/PlaylistButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlaylistDisplayButton from './index'; 3 | 4 | export default { 5 | title: 'Molecules/PlaylistDisplayButton', 6 | component: PlaylistDisplayButton, 7 | }; 8 | 9 | export const Default = () => ; 10 | 11 | export const Open = () => ; 12 | -------------------------------------------------------------------------------- /client/components/molecules/PlaylistDisplayButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import MenuOpenIcon from '@material-ui/icons/MenuOpen'; 4 | 5 | interface DisplayButtonProps { 6 | open: boolean; 7 | onClick? 8 | } 9 | 10 | const Container = styled.div` 11 | width: 100%; 12 | height: 100%; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | cursor: pointer; 17 | color: ${(props) => (props.open ? 'white' : '#747474')}; 18 | background-color: ${(props) => (props.open ? '#ff1150' : 'transparent')}; 19 | `; 20 | 21 | const PlaylistDisplayButton = ({ open, onClick }: DisplayButtonProps) => { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default PlaylistDisplayButton; 30 | -------------------------------------------------------------------------------- /client/components/molecules/ProgressBar/ProgressBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProgressBar from './index'; 3 | 4 | export default { 5 | title: 'Molecules/ProgressBar', 6 | component: ProgressBar, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/SearchInput/SearchInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchInput from './SearchInput'; 3 | 4 | export default { 5 | title: 'Molecules/SearchInput', 6 | component: SearchInput, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/SearchInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './SearchInput'; -------------------------------------------------------------------------------- /client/components/molecules/SlideNextButton/SlideNextButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SlideNextButton from './index'; 3 | 4 | export default { 5 | title: 'Molecules/SlideNextButton', 6 | component: SlideNextButton, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/SlideNextButton/SlideNextButton.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledSlideNextButton = styled.div` 4 | width: 38px; 5 | height: 38px; 6 | border-radius: 70%; 7 | background-color: white; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); 12 | `; 13 | -------------------------------------------------------------------------------- /client/components/molecules/SlideNextButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent } from 'react'; 2 | //import { StyledSlideNextButton } from './SlideNextButton.styles'; 3 | import NavigateNextIcon from '@material-ui/icons/NavigateNext'; 4 | import Circle from '../../atoms/Circle/Circle'; 5 | 6 | interface SlideNextButtonProps { 7 | onClick?: (e: MouseEvent) => void; 8 | } 9 | 10 | const SlideNextButton = ({ onClick }: SlideNextButtonProps) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default SlideNextButton; 19 | -------------------------------------------------------------------------------- /client/components/molecules/SlidePrevButton/SlidePrevButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SlidePrevButton from './index'; 3 | 4 | export default { 5 | title: 'Molecules/SlidePrevButton', 6 | component: SlidePrevButton, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/SlidePrevButton/SlidePrevButton.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledSlidePrevButton = styled.div` 4 | width: 38px; 5 | height: 38px; 6 | border-radius: 70%; 7 | background-color: white; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); 12 | `; 13 | -------------------------------------------------------------------------------- /client/components/molecules/SlidePrevButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent } from 'react'; 2 | //import { StyledSlidePrevButton } from './SlidePrevButton.styles'; 3 | import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; 4 | import Circle from '@components/atoms/Circle/Circle'; 5 | 6 | interface SlidePrevButtonProps { 7 | onClick?: (e: MouseEvent) => void; 8 | } 9 | 10 | const SlidePrevButton = ({ onClick }: SlidePrevButtonProps) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default SlidePrevButton; 19 | -------------------------------------------------------------------------------- /client/components/molecules/SubMenu/SubMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SubMenu from './index'; 3 | 4 | export default { 5 | title: 'Molecules/SubMenu', 6 | component: SubMenu, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/SubMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuLink from '@components/atoms/MenuLink'; 3 | import Text from '@components/atoms/Text'; 4 | import styled from 'styled-components'; 5 | 6 | const MenuTitle = styled.div` 7 | margin: 32px 9px 0; 8 | `; 9 | 10 | const MenuWrapper = styled.div` 11 | & > a { 12 | margin-top: 8px; 13 | } 14 | `; 15 | 16 | const SubMenu = () => ( 17 | 18 | 19 | 보관함 20 | 21 | 믹스테잎 22 | 노래 23 | 아티스트 24 | 앨범 25 | 플레이리스트 26 | 구매한 MP3 27 | 28 | ); 29 | 30 | export default SubMenu; 31 | -------------------------------------------------------------------------------- /client/components/molecules/TrackCard/TrackCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrackCard from '.'; 3 | 4 | export default { 5 | title: 'Molecules/TrackCard', 6 | component: TrackCard, 7 | }; 8 | 9 | 10 | const data = { 11 | id : 1, 12 | title : '그냥', 13 | artistId: 1, 14 | albumId: 1, 15 | lyrics: '가사입니다', 16 | artist: { id: 1, name: '이영지' }, 17 | album: { id: 1, title: '그냥', imageUrl: "https://musicmeta-phinf.pstatic.net/album/004/551/4551646.jpg?type=r720Fll&v=20200507115931" } 18 | } 19 | 20 | export const Playlist = () => ( 21 | 27 | ); 28 | export const NowPlaying = () => ( 29 | 35 | ); 36 | export const Chart = () => ( 37 | 43 | ); 44 | -------------------------------------------------------------------------------- /client/components/molecules/TrackCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import TrackPlayButton from '@components/molecules/TrackPlayButton'; 4 | import TrackInfo from '@components/molecules/TrackInfo'; 5 | import { TrackCardProps } from '@interfaces/props'; 6 | 7 | const TrackCardContainer = styled.div<{ isDefault: boolean }>` 8 | display: flex; 9 | height: 46px; 10 | ${(props) => 11 | props.isDefault && 12 | css` 13 | padding: 8px 0; 14 | height: 56px; 15 | `} 16 | `; 17 | const TrackCard = ({ data, imgVariant, isDefault, isTrack }: TrackCardProps) => { 18 | return ( 19 | 20 | 21 | 22 | 23 | )}; 24 | 25 | export default TrackCard; 26 | -------------------------------------------------------------------------------- /client/components/molecules/TrackInfo/TrackInfo.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrackInfo from '.'; 3 | 4 | export default { 5 | title: 'Molecules/TrackInfo', 6 | component: TrackInfo, 7 | }; 8 | 9 | const data = { 10 | id : 1, 11 | title : '그냥', 12 | artistId: 1, 13 | albumId: 1, 14 | lyrics: '가사입니다', 15 | artist: { id: 1, name: '이영지' }, 16 | album: { id: 1, title: '그냥', imageUrl: "https://musicmeta-phinf.pstatic.net/album/004/551/4551646.jpg?type=r720Fll&v=20200507115931" } 17 | } 18 | 19 | export const Track = () => ; 20 | export const Chart = () => ; 21 | -------------------------------------------------------------------------------- /client/components/molecules/TrackPlayButton/TrackPlayButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrackPlayButton from './index'; 3 | 4 | export default { 5 | title: 'Molecules/TrackPlayButton', 6 | component: TrackPlayButton, 7 | }; 8 | 9 | const data = { 10 | id : 1, 11 | title : '그냥', 12 | artistId: 1, 13 | albumId: 1, 14 | lyrics: '가사입니다', 15 | artist: { id: 1, name: '이영지' }, 16 | album: { id: 1, title: '그냥', imageUrl: "https://musicmeta-phinf.pstatic.net/album/004/551/4551646.jpg?type=r720Fll&v=20200507115931" } 17 | } 18 | 19 | export const TrackRowCard = () => ; 20 | export const TrackInfo = () => ; 21 | -------------------------------------------------------------------------------- /client/components/molecules/TrackPlayButton/TrackPlayButton.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ButtonContainer = styled.button` 4 | position: relative; 5 | outline: 0; 6 | border: none; 7 | background: none; 8 | padding: 0; 9 | width: 40px; 10 | height: 40px; 11 | `; 12 | export const Play = styled.div` 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 40px; 17 | height: 40px; 18 | padding-top: 8px; 19 | visibility: hidden; 20 | opacity: 0%; 21 | background-color: black; 22 | box-sizing: border-box; 23 | `; 24 | -------------------------------------------------------------------------------- /client/components/molecules/VolumnController/VolumnController.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import VolumnController from './index'; 3 | 4 | export default { 5 | title: 'Molecules/VolumnController', 6 | component: VolumnController, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/molecules/VolumnController/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import { Slider } from '@material-ui/core'; 5 | import VolumeUp from '@material-ui/icons/VolumeUp'; 6 | 7 | const VolumnSlider = withStyles({ 8 | root: { 9 | color: '#bcbcbc', 10 | width: 100, 11 | }, 12 | thumb: { 13 | height: 0, 14 | width: 0, 15 | '&:focus, &:hover, &$active': { 16 | boxShadow: 'none', 17 | }, 18 | }, 19 | track: { 20 | height: 4, 21 | }, 22 | rail: { 23 | height: 4, 24 | }, 25 | })(Slider); 26 | 27 | const Container = styled.div` 28 | color: #bcbcbc; 29 | display: flex; 30 | align-items: center; 31 | `; 32 | 33 | const VolumnController = () => { 34 | return ( 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default VolumnController; 43 | -------------------------------------------------------------------------------- /client/components/organisms/ArtistHeader/ArtistHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ArtistHeader from './ArtistHeader'; 3 | 4 | export default { 5 | title: 'Organisms/ArtistHeader', 6 | component: ArtistHeader, 7 | }; 8 | 9 | const STORY_SRC = 'https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg?type=ff300_300&v=20191231151906'; 10 | const STORY_NAME = '이영지'; 11 | const STORY_GENRE = '랩/힙합'; 12 | 13 | export const Default = () => ; 14 | -------------------------------------------------------------------------------- /client/components/organisms/CardListContainer/index.ts: -------------------------------------------------------------------------------- 1 | import CardListContainer from "./CardListContainer"; 2 | 3 | export { default } from './CardListContainer'; -------------------------------------------------------------------------------- /client/components/organisms/CardLists/ChartCardList/ChartCardList.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ChartCardProps } from '@interfaces/props'; 2 | import React from 'react'; 3 | import ChartCardList from '.'; 4 | 5 | export default { 6 | title: 'Organisms/ChartCardList', 7 | conponent: ChartCardList, 8 | }; 9 | let i = 1; 10 | const ChartCards: ChartCardProps[] = Array(30).fill({ 11 | id: 5, 12 | rank: 1, 13 | title: '그냥', 14 | album: { 15 | id: 0, 16 | imageUrl: 'https://musicmeta-phinf.pstatic.net/artist/002/826/2826154.jpg?type=ff300_300&v=20191231151906', 17 | }, 18 | artist: { 19 | id: 0, 20 | name: '이영지', 21 | } 22 | }); 23 | export const Default = () => ; 24 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/ContentsCardList/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './ContentsCardList'; -------------------------------------------------------------------------------- /client/components/organisms/CardLists/GenreCardList/GenreCardList.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GenreCardList from '.'; 3 | 4 | export default { 5 | title: 'Organisms/GenreCardList', 6 | component: GenreCardList, 7 | }; 8 | 9 | const items = [ 10 | {"id":1,"name":"랩/힙합"}, 11 | {"id":2,"name":"댄스"}, 12 | {"id":3,"name":"알앤비/소울"}, 13 | {"id":4,"name":"인디뮤직"}, 14 | {"id":5,"name":"팝"}, 15 | {"id":6,"name":"락"}, 16 | {"id":7,"name":"발라드"}] 17 | 18 | export const Default = () => ; 19 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/MagazineList/MagazineList.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MagazineList from './MagazineList'; 3 | import { MagazineSort } from '@interfaces/props'; 4 | export default { 5 | title: 'Organisms/MagazineList', 6 | component: MagazineList, 7 | }; 8 | 9 | const Magazinesdata = Array(9).fill({ 10 | id: 1, 11 | title: "나만 없어 그 한정판\nLP 레코드", 12 | imageUrl: "https://music-phinf.pstatic.net/20201116_25/1605515795782Xy0Kf_JPEG/0-%B4%EB%C7%A5%C0%CC%B9%CC%C1%F6-%C1%A4%B9%E6%C7%FC_11.jpg?type=w720", 13 | date: "2020-11-19", 14 | category: "gerne" 15 | }); 16 | 17 | 18 | export const Defalut = () => ; 19 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/MagazineList/MagazineList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MagazineCard from '@components/organisms/Cards/MagazineCard'; 3 | import { List, Item } from './MagazineList.styles'; 4 | import { MagazineCardProps } from '@interfaces/props'; 5 | import ComponentInfoWrapper from '@utils/context/ComponentInfoWrapper'; 6 | import { dataType } from '@constants/identifier'; 7 | 8 | interface MagazineListProps { 9 | variant?: string; 10 | items: MagazineCardProps[]; 11 | } 12 | 13 | const MagazineList = ({ variant, items }: MagazineListProps) => ( 14 | 15 | {items.map((item) => ( 16 | 17 | 18 | 19 | 20 | 21 | ))} 22 | 23 | ); 24 | 25 | export default MagazineList; 26 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/PlayerTrackList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { PlayerTrackCardProps } from 'interfaces/props'; 4 | import PlayerTrackCard from '@components/organisms/Cards/PlayerTrackCard'; 5 | import ComponentInfoWrapper from 'utils/context/ComponentInfoWrapper'; 6 | import { dataType } from 'constants/identifier'; 7 | 8 | const ListContainer = styled.ul` 9 | overflow-y: auto; 10 | position: absolute; 11 | top: 0; 12 | bottom: 90px; 13 | right: 0; 14 | z-index: 2; 15 | `; 16 | 17 | const PlayerTrackList = ({ items }: { items: PlayerTrackCardProps[] }) => { 18 | return ( 19 | 20 | {items.map((item) => ( 21 | 22 | 25 | 26 | ))} 27 | 28 | )}; 29 | 30 | export default PlayerTrackList; 31 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/StationCardList/StationCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StationCardList from './StationCardList'; 3 | 4 | export default { 5 | title: 'Organisms/StationCardList', 6 | component: StationCardList, 7 | }; 8 | 9 | const StationCards = Array(20).fill({ 10 | src: 'https://music-phinf.pstatic.net/20181204_11/1543918826895DFvFt_PNG/mood_3_Happy.png?type=f360', 11 | href: '#', 12 | sort: '', 13 | }); 14 | 15 | export const Default = () => ; 16 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/StationCardList/StationCardList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { StationCardProps } from '@interfaces/props'; 4 | import StationCard from '@components/organisms/Cards/StationCard/StationCard'; 5 | 6 | interface StationCardListProps { 7 | items: StationCardProps[]; 8 | } 9 | const ListContainer = styled.ul` 10 | margin: 0; 11 | list-style: none; 12 | padding: 0; 13 | overflow: hidden; 14 | `; 15 | 16 | const StyledList = styled.li` 17 | box-sizing: border-box; 18 | float: left; 19 | padding: 0 16px 16px 0; 20 | width: 196px; 21 | `; 22 | const StationCardList = ({ items }: StationCardListProps) => ( 23 | 24 | {items.map((item, idx) => ( 25 | 26 | 27 | 28 | ))} 29 | 30 | ); 31 | 32 | export default StationCardList; 33 | -------------------------------------------------------------------------------- /client/components/organisms/CardLists/TrackRowList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import TrackRowCard from '@components/organisms/Cards/TrackRowCard'; 4 | import { TrackRowCardProps } from '@interfaces/props'; 5 | import ComponentInfoWrapper from '@utils/context/ComponentInfoWrapper'; 6 | import { dataType } from '@constants/identifier'; 7 | 8 | const ListContainer = styled.ul` 9 | list-style: none; 10 | margin: 0; 11 | padding: 0; 12 | `; 13 | const TrackRowList = ({ items }: { items: TrackRowCardProps[] }) => ( 14 | 15 | {items.map((item) => ( 16 | 17 | 18 | 19 | ))} 20 | 21 | ); 22 | 23 | export default TrackRowList; 24 | -------------------------------------------------------------------------------- /client/components/organisms/CardScrollList/CardScrollList.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.div` 4 | position: relative; 5 | min-height: 100%; 6 | `; 7 | 8 | const ListContainer = styled.div` 9 | overflow-x: auto; 10 | -ms-overflow-style: none; /* IE */ 11 | scrollbar-width: none; /* Firefox */ 12 | &::-webkit-scrollbar { 13 | display: none !important; /* 크롬 등 */ 14 | } 15 | scroll-behavior: smooth; 16 | `; 17 | 18 | const PrevButtonContainer = styled.div` 19 | position: absolute; 20 | top: 50%; 21 | left: -27px; 22 | transform: translateY(-50%); 23 | `; 24 | 25 | const NextButtonContainer = styled.div` 26 | position: absolute; 27 | top: 50%; 28 | right: -27px; 29 | transform: translateY(-50%); 30 | `; 31 | 32 | export { Container, ListContainer, PrevButtonContainer, NextButtonContainer }; 33 | -------------------------------------------------------------------------------- /client/components/organisms/CardScrollList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CardScrollList'; -------------------------------------------------------------------------------- /client/components/organisms/Cards/AlbumCard/AlbumCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AlbumCard from './AlbumCard'; 3 | 4 | export default { 5 | title: 'Organisms/AlbumCard', 6 | component: AlbumCard, 7 | }; 8 | 9 | const Albumdata = { 10 | id: 11, 11 | title: "그냥", 12 | description: "이영지의 새로운 싱글앨범 <그냥>이 발매되었다.\n\n이번 곡은 아티스트 이영지가 그 동안 보여줘 왔던 기존 곡들과는 사뭇 다른 감성으로 우리에게 다가온다.\n\n2019년 11월 첫번째 발표곡 <암실>을 시작으로 약 6개월간 5곡의 작품을 발표한 이영지는 자신의 음악적 스펙트럼을 계속해서 확장해 나가며 다양한 음악을 우리에게 선사하고 있다.\n\n감성짙은 이번 싱글앨범 <그냥>은 우리에게 그녀의 또 다른 새로운 시작을 알리고 있다.", 13 | releaseDate: "2020-05-07", 14 | imageUrl: "https://musicmeta-phinf.pstatic.net/album/004/551/4551646.jpg", 15 | artist: { 16 | id: 3, 17 | name: "이영지" 18 | } 19 | }; 20 | 21 | export const Default = () => ; 22 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/MagazineCard/MagazineCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MagazineCard from './MagazineCard'; 3 | 4 | export default { 5 | title: 'Organisms/MagazineCard', 6 | component: MagazineCard, 7 | }; 8 | 9 | const Magazinedata = 10 | { 11 | id: 1, 12 | title: "나만 없어 그 한정판\nLP 레코드", 13 | imageUrl: "https://music-phinf.pstatic.net/20201116_25/1605515795782Xy0Kf_JPEG/0-%B4%EB%C7%A5%C0%CC%B9%CC%C1%F6-%C1%A4%B9%E6%C7%FC_11.jpg?type=w720", 14 | date: "2020-11-19", 15 | category: "gerne" 16 | } 17 | 18 | export const TodayMagazine = () => ( 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/MagazineCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MagazineCard'; -------------------------------------------------------------------------------- /client/components/organisms/Cards/MainMagazineCard/MainMagazineCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainMagazineCard from './MainMagazineCard'; 3 | 4 | export default { 5 | title: 'Organisms/MainMagazineCard', 6 | component: MainMagazineCard, 7 | }; 8 | 9 | const mainMagazineData = 10 | { 11 | id: 0, 12 | imageUrl: "https://music-phinf.pstatic.net/20201119_255/1605768990292DkTAH_JPEG/%B4%EB%C7%A5-%C0%CC%B9%CC%C1%F61.jpg?type=w720", 13 | title: "차트를 달리는 래퍼 : 잭 할로우, 물라토", 14 | description: "아직 한 달 남짓한 시간이 남았지만, 2020년 역시 힙합의 해라고 해도 과언이 아니지 않을까? 신인을 비롯한 수많은 힙합 아티스트들이 빌보드 HOT 차트 상위권을 거쳐가며 인기를 끌었기 때문이다. 그런데 최근 힙합을 잘 챙겨 듣지 않은 이들에게는 신인의 이름이 낯설 수도 있다. 올해가 가기 전 이름을 알아 두면 좋을 일곱 명의 래퍼를 확인해보자. - 힙합엘이", 15 | date: "2020-11-19", 16 | category: "gerne" 17 | } 18 | 19 | export const TodayMagazine = () => ( 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/MixtapeCard/MixtapeCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MixtapeCard from './MixtapeCard'; 3 | 4 | export default { 5 | title: 'Organisms/MixtapeCard', 6 | component: MixtapeCard, 7 | }; 8 | 9 | const Mixtapedata = { 10 | id: 1, 11 | title: "나를 위한 믹스테잎", 12 | subTitle: "", 13 | description: "Lana Del Rey, Dua Lipa, 이영지", 14 | imageUrl: "https://vibeapp.music.naver.com/vibe/v1/cover/mix/3171155,2487724,3553414,635724/favorite/favorite/", 15 | customized: false 16 | }; 17 | 18 | export const Default = () => ( 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/NewsCard/NewsCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NewsCard from './NewsCard'; 3 | 4 | export default { 5 | title: 'Organisms/NewsCard', 6 | component: NewsCard, 7 | }; 8 | 9 | const Newsdata = 10 | { 11 | id: 2, 12 | title: "블랙핑크가 데뷔 첫 온라인 콘서트를 합니다", 13 | imageUrl: "https://music-phinf.pstatic.net/20201204_242/1607046595052EDJxR_JPEG/blackpink_400.jpg?type=f310_182", 14 | date: "2020-12-06", 15 | link: "https://www.yna.co.kr/view/AKR20201203094500005?section=entertainment/pop-song", 16 | albumId: 9 17 | } 18 | 19 | export const Default = () => ; 20 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/NewsCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NewsCard'; -------------------------------------------------------------------------------- /client/components/organisms/Cards/PlaylistCard/PlaylistCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlaylistCard from './PlaylistCard'; 3 | 4 | export default { 5 | title: 'Organisms/PlaylistCard', 6 | component: PlaylistCard, 7 | }; 8 | 9 | const playlistData = 10 | { 11 | id: 1, 12 | title: "VIBE AND CHILL", 13 | subTitle: "", 14 | description: "VIBE", 15 | imageUrl: "https://music-phinf.pstatic.net/20200504_183/1588567824216rHHs6_PNG/VIBE_%B0%F8%C5%EB_VibeAndChill.png", 16 | customized: false 17 | } 18 | 19 | export const Default = () => ( 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/StationCard/StationCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StationCard from './StationCard'; 3 | 4 | export default { 5 | title: 'Organisms/StationCard', 6 | component: StationCard, 7 | }; 8 | 9 | const STORY_SRC = 'https://music-phinf.pstatic.net/20181204_11/1543918826895DFvFt_PNG/mood_3_Happy.png?type=f360'; 10 | 11 | export const Default = () => ; 12 | -------------------------------------------------------------------------------- /client/components/organisms/Cards/StationCard/StationCard.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import ContentsThumbnail from '@components/molecules/ContentsThumbnail/ContentsThumbnail'; 4 | import { StationCardProps } from 'interfaces/props'; 5 | import A from '@components/atoms/A/A'; 6 | import Text from '@components/atoms/Text'; 7 | 8 | const CardContainer = styled.div` 9 | display: flex; 10 | flex-flow: column; 11 | width: 180px; 12 | height: 180px; 13 | `; 14 | 15 | const ThumbnailContainer = styled.div``; 16 | 17 | const StationCard = ({ src }: StationCardProps) => ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | export default StationCard; 26 | -------------------------------------------------------------------------------- /client/components/organisms/ContentsButtonGroup/ContentsButtonGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContentsButtonGroup from './index'; 3 | 4 | export default { 5 | title: 'Organisms/ContentsButtonGroup', 6 | component: ContentsButtonGroup, 7 | }; 8 | 9 | export const Default = () => ; 10 | -------------------------------------------------------------------------------- /client/components/organisms/ContentsButtonGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import DehazeIcon from '@material-ui/icons/Dehaze'; 4 | import Button from '@components/atoms/Button'; 5 | 6 | const ButtonContainer = styled.div` 7 | width: 260px; 8 | height: 40px; 9 | display: flex; 10 | justify-content: space-between; 11 | `; 12 | 13 | const StyledDehazeIcon = styled(DehazeIcon)` 14 | margin-top: 1px; 15 | `; 16 | 17 | const ContentsButtonGroup = () => { 18 | return ( 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default ContentsButtonGroup; -------------------------------------------------------------------------------- /client/components/organisms/FloatingSelectMenu/FloatingSelectMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import FloatingSelectMenu from './index'; 2 | 3 | export default { 4 | title: 'Organisms/FloatingSelectMenu', 5 | component: FloatingSelectMenu, 6 | }; 7 | 8 | export const Default = () => -------------------------------------------------------------------------------- /client/components/organisms/HeaderButtonGroup/HeaderButtonGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderButtonGroup from './index'; 3 | 4 | export default { 5 | title: 'Organisms/HeaderButtonGroup', 6 | component: HeaderButtonGroup, 7 | }; 8 | 9 | export const Default = () => ; 10 | 11 | export const Track = () => ; 12 | -------------------------------------------------------------------------------- /client/components/organisms/HeaderSideBar/HeaderSideBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderSideBar from '.'; 3 | 4 | export default { 5 | title: 'Organisms/HeaderSideBar', 6 | component: HeaderSideBar, 7 | }; 8 | 9 | export const LoggedIn = () => ; 10 | 11 | export const NotLoggedIn = () => ; 12 | -------------------------------------------------------------------------------- /client/components/organisms/Library/LibraryCardList/LibraryCardList.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ListContainer = styled.div` 4 | width: 960px; 5 | `; 6 | 7 | export const List = styled.ul` 8 | padding: 0; 9 | list-style: none; 10 | display: flex; 11 | flex-flow: wrap; 12 | `; 13 | 14 | export const Item = styled.li` 15 | width: 180px; 16 | margin-left: 10px; 17 | margin-bottom: 20px; 18 | `; 19 | -------------------------------------------------------------------------------- /client/components/organisms/Library/LibraryHeader/LibraryHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LibraryHeader from './LibraryHeader'; 3 | 4 | export default { 5 | title: 'Organisms/LibraryHeader', 6 | component: LibraryHeader, 7 | }; 8 | 9 | export const Mixtape = () => ; 10 | export const Track = () => ; 11 | export const Artist = () => ; 12 | export const Album = () => ; 13 | export const Playlist = () => ; 14 | -------------------------------------------------------------------------------- /client/components/organisms/LyricModal/LyricModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LyricModal from './LyricModal'; 3 | 4 | export default { 5 | title: 'Organisms/LyricModal', 6 | component: LyricModal, 7 | }; 8 | 9 | const data = { 10 | src: 'https://musicmeta-phinf.pstatic.net/album/004/551/4551646.jpg?type=r360Fll&v=20200507115931', 11 | title: '그냥', 12 | artist: '이영지', 13 | lyrics: '가사', 14 | visibility: true 15 | } 16 | 17 | export const Default = () => ( 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /client/components/organisms/MusicPlayer/MusicPlayer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MusicPlayer from './index'; 3 | 4 | export default { 5 | title: 'Organisms/MusicPlayer', 6 | component: MusicPlayer, 7 | }; 8 | 9 | export const Default = () => ; -------------------------------------------------------------------------------- /client/components/organisms/MusicPlayer/PlayerTrackInfo/PlayerTrackInfo.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlayerTrackInfo from './index'; 3 | 4 | export default { 5 | title: 'Organisms/PlayerTrackInfo', 6 | component: PlayerTrackInfo, 7 | }; 8 | 9 | const data = { 10 | id : 1, 11 | title : '그냥', 12 | artistId: 1, 13 | albumId: 1, 14 | lyrics: '가사입니다', 15 | artist: { id: 1, name: '이영지' }, 16 | album: { id: 1, title: '그냥', imageUrl: "https://musicmeta-phinf.pstatic.net/album/004/551/4551646.jpg?type=r720Fll&v=20200507115931" } 17 | } 18 | 19 | export const Default = () => ; -------------------------------------------------------------------------------- /client/components/organisms/PlaylistModal/NewPlaylistButton/NewPlaylistButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import NewPlaylistButton from './index'; 2 | import React from 'react'; 3 | 4 | export default { 5 | title: 'Organisms/NewPlaylistButton', 6 | component: NewPlaylistButton, 7 | }; 8 | 9 | export const Default = () => ( 10 | 11 | ); -------------------------------------------------------------------------------- /client/components/organisms/PlaylistModal/PlaylistAddModal/PlaylistAddModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlaylistAddModal from './index'; 3 | 4 | export default { 5 | title: 'Organisms/PlaylistAddModal', 6 | component: PlaylistAddModal, 7 | }; 8 | 9 | export const Default = () => ( 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /client/components/organisms/PlaylistModal/PlaylistModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlaylistModal from './index'; 3 | 4 | export default { 5 | title: 'Organisms/PlaylistModal', 6 | component: PlaylistModal, 7 | }; 8 | 9 | const data = 10 | { 11 | "id": 1, 12 | "title": "나만 없어 그 한정판 LP 레코드", 13 | "subTitle": "", 14 | "description": null, 15 | "imageUrl": "https://music-phinf.pstatic.net/20200109_13/15785370058255nAVe_PNG/%C0%CC%B4%DE%C0%C7%B3%EB%B7%A1_%C1%A4%B9%E6%C7%FC.png", 16 | "customized": false 17 | } 18 | 19 | const playlistData = Array(15).fill(data); 20 | 21 | export const Default = () => ( 22 | 23 | ); -------------------------------------------------------------------------------- /client/components/organisms/PlaylistModal/PlaylistRowCard/PlaylistRowCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import PlaylistRowCard from './index'; 2 | import React from 'react'; 3 | 4 | export default { 5 | title: 'Organisms/PlaylistRowCard', 6 | component: PlaylistRowCard, 7 | }; 8 | 9 | const data = 10 | { 11 | id: 0, 12 | imageUrl: "https://musicmeta-phinf.pstatic.net/album/002/440/2440250.jpg?type=r420Fll&v=20200218150707", 13 | title: '내가 만든 플레이리스트', 14 | trackCount: 50 15 | }; 16 | 17 | export const Default = () => ( 18 | 19 | ); -------------------------------------------------------------------------------- /client/components/organisms/UserProfileMenu/UserProfileMenu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserProfileMenu from './UserProfileMenu'; 3 | 4 | export default { 5 | title: 'Organisms/UserProfileMenu', 6 | component: UserProfileMenu, 7 | }; 8 | 9 | export const LoggedIn = () => ( 10 |
11 | 19 |
20 | ); 21 | 22 | export const NotLoggedIn = () => 23 | ; 30 | -------------------------------------------------------------------------------- /client/components/organisms/UserProfileMenu/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './UserProfileMenu'; -------------------------------------------------------------------------------- /client/constants/apiUrl.ts: -------------------------------------------------------------------------------- 1 | const host = 'http://localhost:4500'; 2 | const eventHost = 'http://localhost:5500'; 3 | const baseUrl = `${host}/api`; 4 | const eventUrl = `${eventHost}/api`; 5 | const apiUrl = { 6 | magazine: `${baseUrl}/magazines`, 7 | news: `${baseUrl}/news`, 8 | playlist: `${baseUrl}/playlists`, 9 | mixtape: `${baseUrl}/library/mixtapes`, 10 | album: `${baseUrl}/albums`, 11 | track: `${baseUrl}/tracks`, 12 | artist: `${baseUrl}/artists`, 13 | libraryAlbum: `${baseUrl}/library/albums`, 14 | libraryTrack: `${baseUrl}/library/tracks`, 15 | libraryArtist: `${baseUrl}/library/artists`, 16 | libraryPlaylist: `${baseUrl}/library/playlists`, 17 | libraryMixtape: `${baseUrl}/library/mixtapes`, 18 | like: `${baseUrl}/library/`, 19 | addTracksToPlaylist: `${baseUrl}/playlists/tracks`, 20 | user: `${baseUrl}/users`, 21 | login: `${host}/auth`, 22 | chart: `${baseUrl}/chart`, 23 | genre: `${baseUrl}/genres`, 24 | 25 | event: `${eventUrl}/events/`, 26 | playEvent: `${eventUrl}/play-events/`, 27 | }; 28 | 29 | export default apiUrl; 30 | -------------------------------------------------------------------------------- /client/constants/dropDownMenu.ts: -------------------------------------------------------------------------------- 1 | 2 | const dropDownMenu = { 3 | like:'좋아요', 4 | unlike:'좋아요 취소', 5 | addToPlaylist:'내 플레이리스트 추가', 6 | addToLibrary:'보관함에 추가', 7 | addToUpNext:'현재재생목록에 추가', 8 | showLyric:'가사 보기', 9 | buyMP3:'MP3 구매', 10 | share:'공유' 11 | } 12 | 13 | export default dropDownMenu; -------------------------------------------------------------------------------- /client/constants/events.ts: -------------------------------------------------------------------------------- 1 | export const Event = { 2 | click: 'Click', 3 | transition: 'Transition', 4 | like: 'Like', 5 | search: 'Search', 6 | }; 7 | 8 | export const PlayEvent = { 9 | addToUpnext: 'AddToUpnext', 10 | removeFromUpnext: 'RemoveFromUpnext', 11 | play: 'Play', 12 | playNow: 'PlayNow', 13 | }; 14 | -------------------------------------------------------------------------------- /client/hooks/useClickEventLog.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import ComponentInfoContext from '@utils/context/ComponentInfoContext'; 4 | 5 | import eventLogger from '@utils/eventLogger'; 6 | 7 | function useClickEventLog({ userId, href }) { 8 | const componentInfo = useContext(ComponentInfoContext); 9 | const router = useRouter(); 10 | 11 | const logClickEvent = () => { 12 | eventLogger('Click', { 13 | userId, 14 | page: router.asPath, 15 | targetPage: href, 16 | ...componentInfo, 17 | }); 18 | }; 19 | 20 | return logClickEvent; 21 | } 22 | 23 | export default useClickEventLog; 24 | -------------------------------------------------------------------------------- /client/hooks/useInput.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | 3 | export const useInput = (initialValue = null) => { 4 | const [value, setValue] = useState(initialValue); 5 | const handler = useCallback((e) => { 6 | setValue(e.target.value); 7 | }, []); 8 | return [value, handler, setValue]; 9 | } -------------------------------------------------------------------------------- /client/hooks/useLikeEventLog.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import ComponentInfoContext from '@utils/context/ComponentInfoContext'; 3 | 4 | import eventLogger from '@utils/eventLogger'; 5 | 6 | function useLikeEventLog({ userId }) { 7 | const componentInfo = useContext(ComponentInfoContext); 8 | 9 | const logLikeEvent = (isLike) => { 10 | eventLogger('Like', { 11 | userId, 12 | isLike, 13 | ...componentInfo, 14 | }); 15 | }; 16 | 17 | return logLikeEvent; 18 | } 19 | 20 | export default useLikeEventLog; 21 | -------------------------------------------------------------------------------- /client/hooks/usePlayEventLog.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import ComponentInfoContext from '@utils/context/ComponentInfoContext'; 3 | 4 | import eventLogger from '@utils/eventLogger'; 5 | 6 | function usePlayEventLog({ userId }) { 7 | const componentInfo = useContext(ComponentInfoContext); 8 | 9 | const logPlayEvent = (trackId, isPlay) => { 10 | eventLogger('Play', { 11 | userId, 12 | trackId, 13 | isPlay, 14 | ...componentInfo, 15 | }); 16 | }; 17 | 18 | return logPlayEvent; 19 | } 20 | 21 | export default usePlayEventLog; 22 | -------------------------------------------------------------------------------- /client/hooks/usePlayNowEventLog.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import ComponentInfoContext from '@utils/context/ComponentInfoContext'; 3 | 4 | import eventLogger from '@utils/eventLogger'; 5 | 6 | function usePlayNowEventLog({ userId }) { 7 | const componentInfo = useContext(ComponentInfoContext); 8 | 9 | const logPlayNowEvent = (targetTrackId, trackId?, playtime?, totalPlaytime?) => { 10 | const playingProgress = Math.floor((playtime * 100) / totalPlaytime); 11 | eventLogger('PlayNow', { 12 | userId, 13 | targetTrackId, 14 | trackId, 15 | playingProgress, 16 | ...componentInfo, 17 | }); 18 | }; 19 | 20 | return logPlayNowEvent; 21 | } 22 | 23 | export default usePlayNowEventLog; 24 | -------------------------------------------------------------------------------- /client/hooks/useSearchEventLog.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import ComponentInfoContext from '@utils/context/ComponentInfoContext'; 3 | 4 | import eventLogger from '@utils/eventLogger'; 5 | 6 | function useSearchEventLog({ userId }) { 7 | const componentInfo = useContext(ComponentInfoContext); 8 | 9 | const logSearchEvent = (text) => { 10 | eventLogger('Search', { 11 | userId, 12 | text, 13 | ...componentInfo, 14 | }); 15 | }; 16 | 17 | return logSearchEvent; 18 | } 19 | 20 | export default useSearchEventLog; 21 | -------------------------------------------------------------------------------- /client/hooks/useTransitionEventLog.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | 4 | import eventLogger from '@utils/eventLogger'; 5 | 6 | function useClickEventLog({ userId }) { 7 | const router = useRouter(); 8 | 9 | const handleRouteChange = (url) => { 10 | eventLogger('Transition', { 11 | userId, 12 | page: url, 13 | }); 14 | }; 15 | 16 | useEffect(() => { 17 | router.events.on('routeChangeComplete', handleRouteChange); 18 | 19 | return () => { 20 | router.events.off('routeChangeComplete', handleRouteChange); 21 | }; 22 | }, []); 23 | } 24 | 25 | export default useClickEventLog; 26 | -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /client/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { HYDRATE } from 'next-redux-wrapper'; 2 | import { combineReducers } from 'redux'; 3 | import user from './user'; 4 | import selectedTrack from './selectedTrack'; 5 | import musicPlayer from './musicPlayer'; 6 | import playlist from './playlist'; 7 | 8 | const rootReducer = (state, action) => { 9 | switch (action.type) { 10 | case HYDRATE: 11 | return action.payload; 12 | default: { 13 | const combinedReducer = combineReducers({ 14 | user, 15 | selectedTrack, 16 | musicPlayer, 17 | playlist 18 | }); 19 | return combinedReducer(state, action); 20 | } 21 | } 22 | }; 23 | 24 | export default rootReducer; 25 | -------------------------------------------------------------------------------- /client/reducers/playlist.ts: -------------------------------------------------------------------------------- 1 | import { SHOW_PLAYLIST_MODAL, HIDE_PLAYLIST_MODAL } from '@constants/actions'; 2 | 3 | interface PlayList { 4 | showModal: boolean; 5 | } 6 | 7 | const initialState = { 8 | showModal: false 9 | } 10 | 11 | export const showPlaylistAddModal = () => { 12 | return { 13 | type: SHOW_PLAYLIST_MODAL 14 | } 15 | } 16 | 17 | export const hidePlaylistAddModal = () => { 18 | return { 19 | type: HIDE_PLAYLIST_MODAL 20 | } 21 | } 22 | 23 | const reducer = (state: PlayList = initialState, action) => { 24 | switch (action.type) { 25 | case SHOW_PLAYLIST_MODAL: 26 | return { 27 | ...state, 28 | showModal: true 29 | } 30 | case HIDE_PLAYLIST_MODAL: 31 | return { 32 | ...state, 33 | showModal: false 34 | } 35 | default: 36 | return state; 37 | } 38 | } 39 | 40 | export default reducer; -------------------------------------------------------------------------------- /client/sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all, fork } from 'redux-saga/effects'; 2 | import axios from 'axios'; 3 | 4 | import musicPlayerSaga from './musicPlayer'; 5 | import userSaga from './user'; 6 | 7 | export default function* rootSaga() { 8 | yield all([ 9 | fork(musicPlayerSaga), 10 | fork(userSaga), 11 | ]); 12 | } -------------------------------------------------------------------------------- /client/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { createWrapper } from 'next-redux-wrapper'; 2 | import { applyMiddleware, createStore, compose } from 'redux'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import createSagaMiddleware from 'redux-saga'; 5 | 6 | import reducer from '../reducers'; 7 | import rootSaga from '../sagas'; 8 | 9 | const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => { 10 | console.log(action); 11 | return next(action); 12 | }; 13 | 14 | const configureStore = () => { 15 | const sagaMiddleware = createSagaMiddleware(); 16 | const middlewares = [sagaMiddleware, loggerMiddleware]; 17 | const enhancer = process.env.NODE_ENV === 'production' 18 | ? compose(applyMiddleware(...middlewares)) 19 | : composeWithDevTools(applyMiddleware(...middlewares)); 20 | const store = createStore(reducer, enhancer); 21 | (store as any).sagaTask = sagaMiddleware.run(rootSaga); 22 | return store; 23 | }; 24 | 25 | const wrapper = createWrapper(configureStore, { 26 | debug: process.env.NODE_ENV === 'development', 27 | }); 28 | 29 | export default wrapper; -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "baseUrl": ".", 21 | "paths": { 22 | "@components/*": [ 23 | "components/*" 24 | ], 25 | "@utils/*":["utils/*"], 26 | "@constants/*":["constants/*"], 27 | "@hooks/*":["hooks/*"], 28 | "@reducers/*":["reducers/*"], 29 | "@stores/*":["stores/*"], 30 | "@interfaces/*":["interfaces/*"] 31 | } 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", "utils/apis.js" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /client/utils/Timer.ts: -------------------------------------------------------------------------------- 1 | class Timer { 2 | timer: number; 3 | func: Function; 4 | delay: number; 5 | 6 | constructor(func, time) { 7 | this.func = func; 8 | this.delay = time; 9 | this.timer = setInterval(func, time); 10 | } 11 | 12 | start() { 13 | if (this.timer) return; 14 | this.timer = setInterval(this.func, this.delay); 15 | } 16 | stop() { 17 | if (!this.timer) return; 18 | clearInterval(this.timer); 19 | this.timer = null; 20 | } 21 | 22 | restart(time = this.delay) { 23 | this.delay = time; 24 | this.stop(); 25 | this.start(); 26 | } 27 | } 28 | 29 | export default Timer; 30 | -------------------------------------------------------------------------------- /client/utils/color.ts: -------------------------------------------------------------------------------- 1 | function getRandomHex() { 2 | return Math.floor(Math.random() * 255).toString(16); 3 | } 4 | 5 | function getRandomColor() { 6 | const r = getRandomHex().padStart(2, '0'); 7 | const g = getRandomHex().padStart(2, '0'); 8 | const b = getRandomHex().padStart(2, '0'); 9 | return `#${r}${g}${b}`; 10 | } 11 | 12 | export { getRandomColor }; 13 | -------------------------------------------------------------------------------- /client/utils/context/ComponentInfoContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface ComponentInfo { 4 | componentId: string; 5 | data?: { 6 | type: string; 7 | id: number; 8 | }; 9 | } 10 | 11 | const ComponentInfoContext = createContext({ componentId: '' }); 12 | 13 | export default ComponentInfoContext; 14 | -------------------------------------------------------------------------------- /client/utils/context/ComponentInfoWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, ReactNode } from 'react'; 2 | import ComponentInfoContext from './ComponentInfoContext'; 3 | 4 | interface IProps { 5 | children: ReactNode; 6 | componentId?: string; 7 | data?: { 8 | type: string; 9 | id: number; 10 | }; 11 | } 12 | 13 | const ComponentInfoWrapper = ({ children, componentId, data }: IProps) => { 14 | const componentInfo = useContext(ComponentInfoContext); 15 | const newComponentInfo = { ...componentInfo }; 16 | if (componentId) newComponentInfo.componentId += `_${componentId}`; 17 | if (data) newComponentInfo.data = data; 18 | 19 | return {children}; 20 | }; 21 | 22 | export default ComponentInfoWrapper; 23 | -------------------------------------------------------------------------------- /client/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'cookies'; 2 | 3 | export const getCookie = (key) => { 4 | const value = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)'); 5 | return value ? value[2] : null; 6 | }; 7 | 8 | export const getTokenFromCtx = ({ req, res }) => { 9 | const cookies = new Cookies(req, res); 10 | const token = cookies.get('token'); 11 | return token; 12 | }; 13 | -------------------------------------------------------------------------------- /client/utils/eventLogger.ts: -------------------------------------------------------------------------------- 1 | import { sendEvent, sendPlayEvent } from './apis'; 2 | import { Event, PlayEvent } from '@constants/events'; 3 | import { getCurrentDate } from './time'; 4 | 5 | function eventLogger(event, param) { 6 | const eventData = { 7 | event, 8 | ...param, 9 | platform: 'Web', 10 | timestamp: getCurrentDate(), 11 | }; 12 | if (Object.values(Event).indexOf(event) !== -1) sendEvent(eventData); 13 | else if (Object.values(PlayEvent).indexOf(event) !== -1) sendPlayEvent(eventData); 14 | } 15 | 16 | export default eventLogger; 17 | -------------------------------------------------------------------------------- /client/utils/time.ts: -------------------------------------------------------------------------------- 1 | export function convertToHHSS(secs: number): string { 2 | return new Date(secs * 1000).toISOString().substr(14, 5); 3 | } 4 | 5 | export function getCurrentDate() { 6 | const date = new Date(); 7 | const year = date.getFullYear(); 8 | const month = date.getMonth(); 9 | const day = date.getDate(); 10 | const hours = date.getHours(); 11 | const minutes = date.getMinutes(); 12 | const seconds = date.getSeconds(); 13 | const milliseconds = date.getMilliseconds(); 14 | 15 | return new Date(Date.UTC(year, month, day, hours, minutes, seconds, milliseconds)); 16 | } 17 | -------------------------------------------------------------------------------- /eventSever/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ['airbnb-base'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | rules: { 14 | indent: ['error', 4], 15 | 'import/prefer-default-export': 'off', 16 | 'no-unused-vars': 'off', 17 | 'no-console': 'off', 18 | 'import/extensions': [ 19 | 'error', 20 | 'ignorePackages', 21 | { 22 | js: 'never', 23 | ts: 'never', 24 | }, 25 | ], 26 | }, 27 | settings: { 28 | 'import/resolver': { 29 | node: { 30 | extensions: ['.js', '.ts'], 31 | }, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /eventSever/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "semi": true, 12 | "endOfLine": "auto" 13 | } -------------------------------------------------------------------------------- /eventSever/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/app.ts", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/express": "^4.17.9", 15 | "body-parser": "^1.19.0", 16 | "cookie-parser": "^1.4.5", 17 | "cors": "^2.8.5", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "mongoose": "^5.10.16", 21 | "morgan": "^1.10.0", 22 | "typescript": "^4.1.2" 23 | }, 24 | "devDependencies": { 25 | "@types/cookie-parser": "^1.4.2", 26 | "@types/cors": "^2.8.8", 27 | "@types/mongoose": "^5.10.1", 28 | "@types/morgan": "^1.9.2", 29 | "@types/node": "^14.14.10", 30 | "@typescript-eslint/eslint-plugin": "^4.9.0", 31 | "@typescript-eslint/parser": "^4.9.0", 32 | "eslint": "^7.14.0", 33 | "eslint-config-airbnb-base": "^14.2.1", 34 | "eslint-plugin-import": "^2.22.1", 35 | "nodemon": "^2.0.6", 36 | "ts-node": "^9.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /eventSever/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cookieParser from 'cookie-parser'; 3 | import cors from 'cors'; 4 | import logger from 'morgan'; 5 | import 'dotenv/config'; 6 | import db from './db'; 7 | import router from './routes'; 8 | 9 | db(); 10 | const app = express(); 11 | 12 | app.use(logger('dev')); 13 | app.use(express.json()); 14 | app.use(express.urlencoded({ extended: false })); 15 | app.use(cookieParser()); 16 | app.use(cors({ 17 | origin: process.env.CLIENT_HOST, 18 | credentials: true, 19 | })); 20 | 21 | app.use('/api', router); 22 | 23 | app.set('port', process.env.PORT || 3000); 24 | app.listen(app.get('port'), () => { 25 | console.log(`API Server App Listening on PORT ${app.get('port')}`); 26 | }); 27 | -------------------------------------------------------------------------------- /eventSever/src/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export default () => { 4 | const dbConnect = () => { 5 | mongoose.connect(`${process.env.MONGO_URI}`, { 6 | dbName: process.env.DB_NAME, 7 | useCreateIndex: true, 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | }) 11 | .then(() => console.log('mongodb connected')) 12 | .catch((e) => console.error(e)); 13 | }; 14 | 15 | dbConnect(); 16 | mongoose.connection.on('disconnected', dbConnect); 17 | }; 18 | -------------------------------------------------------------------------------- /eventSever/src/routes/events/events.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import Event from '../../models/Event'; 3 | 4 | export const create = async (req: Request, res: Response, next: NextFunction) => { 5 | const event = req.body; 6 | 7 | try { 8 | await Event.create(event); 9 | } catch (err) { 10 | switch (err.name) { 11 | case 'ValidationError': 12 | return res.status(400).json({ success: false, message: 'Invalid Parameters' }); 13 | case 'MongooseError': 14 | return res.status(400).json({ success: false, message: 'Invalid Event name' }); 15 | default: 16 | return res.status(500).json({ success: false, message: 'Sever is something wrong' }); 17 | } 18 | } 19 | 20 | return res.json({ 21 | success: true, 22 | }); 23 | }; 24 | 25 | export const list = async (req: Request, res: Response, next: NextFunction) => { 26 | try { 27 | const data = await Event.find({}).sort({ timestamp: -1 }); 28 | return res.json({ 29 | success: true, 30 | data, 31 | }); 32 | } catch (err) { 33 | return res.status(500).json({ success: false }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /eventSever/src/routes/events/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as eventController from './events.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', eventController.create); 7 | router.get('/', eventController.list); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /eventSever/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import eventRouter from './events'; 3 | import playEventRouter from './playEvents'; 4 | 5 | const router = express.Router(); 6 | 7 | router.use('/events', eventRouter); 8 | router.use('/play-events', playEventRouter); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /eventSever/src/routes/playEvents/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as eventController from './playEvents.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', eventController.create); 7 | router.get('/', eventController.list); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /eventSever/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6"], 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 6 | "moduleResolution": "node", 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 9 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 10 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 11 | "outDir": "./build", 12 | "sourceMap": true, 13 | "strictPropertyInitialization": false, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "downlevelIteration": true 17 | }, 18 | "exclude": ["node_modules/"] 19 | } 20 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ['airbnb-base'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | rules: { 14 | indent: ['error', 4], 15 | 'import/prefer-default-export': 'off', 16 | 'no-unused-vars': 'off', 17 | 'no-console': 'off', 18 | 'import/extensions': [ 19 | 'error', 20 | 'ignorePackages', 21 | { 22 | js: 'never', 23 | ts: 'never', 24 | }, 25 | ], 26 | }, 27 | settings: { 28 | 'import/resolver': { 29 | node: { 30 | extensions: ['.js', '.ts'], 31 | }, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "semi": true, 12 | "endOfLine": "auto" 13 | } -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cookieParser from 'cookie-parser'; 3 | import cors from 'cors'; 4 | import logger from 'morgan'; 5 | import passport from 'passport'; 6 | import 'dotenv/config'; 7 | import passportInit from './routes/auth/passport/passport-init'; 8 | import apiRouter from './routes'; 9 | import authRouter from './routes/auth'; 10 | import db from './db'; 11 | 12 | db(); 13 | const app = express(); 14 | 15 | app.use(logger('dev')); 16 | app.use(express.json()); 17 | app.use(express.urlencoded({ extended: false })); 18 | app.use(cookieParser()); 19 | app.use(cors({ 20 | origin: process.env.CLIENT_HOST, 21 | credentials: true, 22 | })); 23 | app.use(passport.initialize()); 24 | passportInit(passport); 25 | 26 | app.use('/auth', authRouter); 27 | app.use('/api', apiRouter); 28 | 29 | app.set('port', process.env.PORT || 3000); 30 | app.listen(app.get('port'), () => { 31 | console.log(`API Server App Listening on PORT ${app.get('port')}`); 32 | }); 33 | -------------------------------------------------------------------------------- /server/src/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { createConnection } from 'typeorm'; 3 | 4 | export default () => { 5 | const dbConnect = () => { 6 | mongoose.connect(`${process.env.MONGO_URI}`, { 7 | dbName: process.env.DB_NAME, 8 | useCreateIndex: true, 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | }) 12 | .then(() => console.log('mongodb connected')) 13 | .catch((e) => console.error(e)); 14 | 15 | createConnection() 16 | .then(() => console.log('mysql connected')) 17 | .catch((e) => console.error(e)); 18 | }; 19 | 20 | dbConnect(); 21 | mongoose.connection.on('disconnected', dbConnect); 22 | }; 23 | -------------------------------------------------------------------------------- /server/src/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import passport from 'passport'; 3 | 4 | export const authenticateJWT = (req : Request, res: Response, next: NextFunction) => { 5 | passport.authenticate('jwt', { session: false }, (err, user) => { 6 | if (err) return next(err); 7 | if (!user) return next(); 8 | 9 | req.user = user.id; 10 | return next(); 11 | })(req, res, next); 12 | }; 13 | 14 | export const checkAuth = (req: Request, res: Response, next: NextFunction) => { 15 | const { user } = req; 16 | if (!user) return res.status(401).json({ success: false, message: 'Login Required' }); 17 | 18 | return next(); 19 | }; 20 | -------------------------------------------------------------------------------- /server/src/models/Album.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, 3 | } from 'typeorm'; 4 | import Genre from './Genre'; 5 | import Artist from './Artist'; 6 | import Track from './Track'; 7 | 8 | @Entity() 9 | class Album { 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column() 14 | title: string; 15 | 16 | @Column() 17 | description: string; 18 | 19 | @Column('date') 20 | releaseDate: Date; 21 | 22 | @Column() 23 | imageUrl: string; 24 | 25 | @ManyToOne((type) => Genre, (genre) => genre.albums, { nullable: false }) 26 | genre: Genre; 27 | 28 | @ManyToOne((type) => Artist, (artist) => artist.albums, { nullable: false }) 29 | artist: Artist; 30 | 31 | @OneToMany((type) => Track, (track) => track.album) 32 | tracks: Track[]; 33 | } 34 | 35 | export default Album; 36 | -------------------------------------------------------------------------------- /server/src/models/Artist.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, 3 | } from 'typeorm'; 4 | import Genre from './Genre'; 5 | import Album from './Album'; 6 | import Track from './Track'; 7 | 8 | @Entity() 9 | class Artist { 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column() 14 | name: string; 15 | 16 | @Column() 17 | imageUrl: string; 18 | 19 | @ManyToOne((type) => Genre, (genre) => genre.artists, { nullable: false }) 20 | genre: Genre; 21 | 22 | @OneToMany((type) => Album, (album) => album.artist) 23 | albums: Album[]; 24 | 25 | @OneToMany((type) => Track, (track) => track.artist) 26 | tracks: Track[]; 27 | } 28 | 29 | export default Artist; 30 | -------------------------------------------------------------------------------- /server/src/models/Genre.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, OneToMany, 3 | } from 'typeorm'; 4 | import Artist from './Artist'; 5 | import Album from './Album'; 6 | import Track from './Track'; 7 | 8 | @Entity() 9 | class Genre { 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column() 14 | name: string; 15 | 16 | @OneToMany((type) => Artist, (artist) => artist.genre) 17 | artists: Artist[]; 18 | 19 | @OneToMany((type) => Album, (album) => album.genre) 20 | albums: Album[]; 21 | 22 | @OneToMany((type) => Track, (track) => track.genre) 23 | tracks: Track[]; 24 | } 25 | 26 | export default Genre; 27 | -------------------------------------------------------------------------------- /server/src/models/Magazine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, 3 | } from 'typeorm'; 4 | import Playlist from './Playlist'; 5 | 6 | @Entity() 7 | class Magazine { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | title: string; 13 | 14 | @Column() 15 | imageUrl: string; 16 | 17 | @Column('text') 18 | description: string; 19 | 20 | @Column() 21 | subTitle: string; 22 | 23 | @Column('text') 24 | content: string; 25 | 26 | @Column('date') 27 | date: Date; 28 | 29 | @Column() 30 | category: string; 31 | 32 | @OneToOne(() => Playlist, { nullable: false }) 33 | @JoinColumn() 34 | playlist: Playlist; 35 | } 36 | 37 | export default Magazine; 38 | -------------------------------------------------------------------------------- /server/src/models/News.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, 3 | } from 'typeorm'; 4 | import Album from './Album'; 5 | 6 | @Entity() 7 | class News { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | title: string; 13 | 14 | @Column() 15 | imageUrl: string; 16 | 17 | @Column('date') 18 | date: Date; 19 | 20 | @Column() 21 | link: string; 22 | 23 | @OneToOne(() => Album, { nullable: false }) 24 | @JoinColumn() 25 | album: Album; 26 | 27 | @Column() 28 | albumId: number; 29 | } 30 | 31 | export default News; 32 | -------------------------------------------------------------------------------- /server/src/models/Playlist.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinTable, 3 | } from 'typeorm'; 4 | import Track from './Track'; 5 | 6 | @Entity() 7 | class Playlist { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | title: string; 13 | 14 | @Column() 15 | subTitle: string; 16 | 17 | @Column({ nullable: true }) 18 | description: string; 19 | 20 | @Column() 21 | imageUrl: string; 22 | 23 | @Column() 24 | customized: boolean; // true: user가 만든 playlist, false: vibe에서 제공하는 playlist 25 | 26 | @ManyToMany(() => Track) 27 | @JoinTable({ name: 'playlist_track' }) 28 | tracks: Track[]; 29 | } 30 | 31 | export default Playlist; 32 | -------------------------------------------------------------------------------- /server/src/models/Track.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, 3 | } from 'typeorm'; 4 | import Genre from './Genre'; 5 | import Artist from './Artist'; 6 | import Album from './Album'; 7 | import User from './User'; 8 | 9 | @Entity() 10 | class Track { 11 | @PrimaryGeneratedColumn() 12 | id: number; 13 | 14 | @Column() 15 | title: string; 16 | 17 | @Column('text') 18 | lyrics: string; 19 | 20 | @Column() 21 | playtime: number; 22 | 23 | @ManyToOne((type) => Genre, (genre) => genre.tracks, { nullable: false }) 24 | genre: Genre; 25 | 26 | @ManyToOne((type) => Artist, (artist) => artist.tracks, { nullable: false }) 27 | artist: Artist; 28 | 29 | @ManyToOne((type) => Album, (album) => album.tracks, { nullable: false }) 30 | album: Album; 31 | 32 | @ManyToMany(() => User, (user) => user.libraryTracks) 33 | likeUsers: User[]; 34 | 35 | @Column() 36 | albumId: number; 37 | } 38 | 39 | export default Track; 40 | -------------------------------------------------------------------------------- /server/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, JoinTable, 3 | } from 'typeorm'; 4 | import Playlist from './Playlist'; 5 | import Track from './Track'; 6 | import Album from './Album'; 7 | import Artist from './Artist'; 8 | 9 | @Entity() 10 | class User { 11 | @PrimaryGeneratedColumn() 12 | id!: number; 13 | 14 | @Column() 15 | email: string; 16 | 17 | @Column() 18 | password: string; 19 | 20 | @Column() 21 | name: string; 22 | 23 | @Column() 24 | imageUrl: string; 25 | 26 | @ManyToMany(() => Track, (track) => track.likeUsers) 27 | @JoinTable({ name: 'library_tracks' }) 28 | libraryTracks: Track[]; 29 | 30 | @ManyToMany(() => Album) 31 | @JoinTable({ name: 'library_albums' }) 32 | libraryAlbums: Album[]; 33 | 34 | @ManyToMany(() => Artist) 35 | @JoinTable({ name: 'library_artists' }) 36 | libraryArtists: Artist[]; 37 | 38 | @ManyToMany(() => Playlist) 39 | @JoinTable({ name: 'library_playlists' }) 40 | libraryPlaylists: Playlist[]; 41 | } 42 | 43 | export default User; 44 | -------------------------------------------------------------------------------- /server/src/routes/albums/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as albumController from './albums.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', albumController.list); 7 | router.get('/:id', albumController.findOne); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/routes/artists/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as artistController from './artists.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', artistController.list); 7 | router.get('/:id', artistController.findOne); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/routes/auth/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import passport from 'passport'; 3 | import * as authController from './auth.controller'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/naver/callback', authController.authenticateLogin); 8 | router.get('/naver-login', passport.authenticate('naver'), authController.naverLogin); 9 | router.post('/local-login', authController.authenticateLogin); 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/routes/auth/passport/jwt-strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStatic } from 'passport'; 2 | import passportJwt, { ExtractJwt } from 'passport-jwt'; 3 | import { getManager } from 'typeorm'; 4 | import User from '../../../models/User'; 5 | 6 | const JwtStrategy = passportJwt.Strategy; 7 | const config = { 8 | jwtFromRequest: ExtractJwt.fromHeader('authorization'), 9 | secretOrKey: process.env.JWT_SECRET, 10 | }; 11 | 12 | const auth = async (payload: any, done:any) => { 13 | try { 14 | const manager = getManager(); 15 | const user = await manager.findOne(User, payload.id); 16 | if (!user) return done(null, false); 17 | 18 | return done(null, { id: user.id }); 19 | } catch (err) { 20 | console.error(err); 21 | return done(null, false); 22 | } 23 | }; 24 | 25 | export const jwtStrategy = (passport: PassportStatic) => { 26 | passport.use('jwt', new JwtStrategy(config, auth)); 27 | }; 28 | -------------------------------------------------------------------------------- /server/src/routes/auth/passport/local-strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStatic } from 'passport'; 2 | import passportLocal from 'passport-local'; 3 | import { getManager } from 'typeorm'; 4 | import bcrypt from 'bcrypt'; 5 | import User from '../../../models/User'; 6 | 7 | const Localstrategy = passportLocal.Strategy; 8 | const config = { 9 | usernameField: 'email', 10 | passwordField: 'pw', 11 | }; 12 | 13 | const auth = async (id:any, password:any, done:any) => { 14 | try { 15 | const manager = getManager(); 16 | const user = await manager.findOne(User, { email: id }); 17 | 18 | if (!user) return done(null, { success: false, message: '존재하지 않는 사용자입니다.' }); 19 | const result = await bcrypt.compare(password, user.password); 20 | if (!result) return done(null, { success: false, message: '잘못된 비밀번호입니다.' }); 21 | return done(null, { success: true, user }); 22 | } catch (err) { 23 | console.error(err); 24 | return done(err, { success: false, messasge: err }); 25 | } 26 | }; 27 | 28 | export const localStrategy = (passport : PassportStatic) => { 29 | passport.use('local-login', new Localstrategy(config, auth)); 30 | }; 31 | -------------------------------------------------------------------------------- /server/src/routes/auth/passport/passport-init.ts: -------------------------------------------------------------------------------- 1 | import { PassportStatic } from 'passport'; 2 | import { naverStrategy } from './naver-strategy'; 3 | import { jwtStrategy } from './jwt-strategy'; 4 | import { localStrategy } from './local-strategy'; 5 | 6 | const passportInit = (pp : PassportStatic) => { 7 | naverStrategy(pp); 8 | jwtStrategy(pp); 9 | localStrategy(pp); 10 | }; 11 | 12 | export default passportInit; 13 | -------------------------------------------------------------------------------- /server/src/routes/chart/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as chartController from './chart.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', chartController.list); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/src/routes/genres/genres.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { getRepository } from 'typeorm'; 3 | 4 | import Genre from '../../models/Genre'; 5 | 6 | const list = async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const GenreRepository = getRepository(Genre); 9 | const genres = await GenreRepository.find(); 10 | 11 | return res.json({ 12 | success: true, 13 | data: [...genres], 14 | }); 15 | } catch (err) { 16 | return res.status(500).json({ success: false }); 17 | } 18 | }; 19 | 20 | export { list }; 21 | -------------------------------------------------------------------------------- /server/src/routes/genres/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as genreController from './genres.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', genreController.list); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import userRouter from './users'; 3 | import artistRouter from './artists'; 4 | import newsRouter from './news'; 5 | import magazineRouter from './magazines'; 6 | import libraryRouter from './library'; 7 | import playlistRouter from './playlists'; 8 | import albumRouter from './albums'; 9 | import trackRouter from './tracks'; 10 | import mixtapeRouter from './mixtapes'; 11 | import chartRouter from './chart'; 12 | import genreRouter from './genres'; 13 | import { authenticateJWT, checkAuth } from '../middlewares/auth'; 14 | 15 | const router = express.Router(); 16 | 17 | router.use(authenticateJWT); 18 | router.use('/users', userRouter); 19 | router.use('/artists', artistRouter); 20 | router.use('/news', newsRouter); 21 | router.use('/magazines', magazineRouter); 22 | router.use('/library', checkAuth, libraryRouter); 23 | router.use('/playlists', playlistRouter); 24 | router.use('/tracks', trackRouter); 25 | router.use('/albums', albumRouter); 26 | router.use('/mixtapes', mixtapeRouter); 27 | router.use('/chart', chartRouter); 28 | router.use('/genres', genreRouter); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /server/src/routes/library/album/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as albumController from './library.album.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', albumController.list); 7 | router.post('/', albumController.create); 8 | router.delete('/:albumId', albumController.remove); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/routes/library/artist/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as artistController from './library.artist.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', artistController.list); 7 | router.post('/', artistController.create); 8 | router.delete('/:artistId', artistController.remove); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/routes/library/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import trackRouter from './tracks'; 3 | import playlistsRouter from './playlists'; 4 | import albumRouter from './album'; 5 | import artistRouter from './artist'; 6 | import mixtapeRouter from './mixtapes'; 7 | 8 | const router = express.Router(); 9 | 10 | router.use('/tracks', trackRouter); 11 | router.use('/playlists', playlistsRouter); 12 | router.use('/albums', albumRouter); 13 | router.use('/artists', artistRouter); 14 | router.use('/mixtapes', mixtapeRouter); 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /server/src/routes/library/mixtapes/index.ts: -------------------------------------------------------------------------------- 1 | import { log } from 'console'; 2 | import express from 'express'; 3 | import * as mixtapesController from './library.mixtapes.controller'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', mixtapesController.list); 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/src/routes/library/mixtapes/library.mixtapes.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { getRepository } from 'typeorm'; 3 | import User from '../../../models/User'; 4 | 5 | const list = async (req: Request, res: Response, next: NextFunction) => { 6 | const userId = req.user; 7 | try { 8 | const UserRepository = getRepository(User); 9 | const user = await UserRepository.createQueryBuilder('user') 10 | .leftJoinAndSelect('user.libraryPlaylists', 'library_playlists') 11 | .select([ 12 | 'user.id', 13 | 'library_playlists.id', 14 | 'library_playlists.title', 15 | 'library_playlists.subTitle', 16 | 'library_playlists.imageUrl', 17 | ]) 18 | .where('user.id = :id', { id: userId }) 19 | .where('mixtape = 1') 20 | .getOne(); 21 | 22 | const libraryPlaylists = user?.libraryPlaylists || []; 23 | return res.json({ success: true, data: libraryPlaylists }); 24 | } catch (err) { 25 | console.error(err); 26 | return res.status(500).json({ success: false }); 27 | } 28 | }; 29 | 30 | export { list }; 31 | -------------------------------------------------------------------------------- /server/src/routes/library/playlists/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as playlistsController from './library.playlists.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', playlistsController.list); 7 | router.post('/', playlistsController.create); 8 | router.delete('/:playlistId', playlistsController.remove); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/routes/library/tracks/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as trackController from './library.tracks.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', trackController.list); 7 | router.post('/', trackController.create); 8 | router.delete('/:trackId', trackController.remove); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/src/routes/magazines/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as magazineController from './magazines.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', magazineController.list); 7 | router.get('/:id', magazineController.findOne); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/routes/mixtapes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as mixtapesController from './mixtapes.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', mixtapesController.list); 7 | export default router; 8 | -------------------------------------------------------------------------------- /server/src/routes/mixtapes/mixtapes.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { getRepository } from 'typeorm'; 3 | import Playlist from '../../models/Playlist'; 4 | 5 | const list = async (req: Request, res: Response, next: NextFunction) => { 6 | try { 7 | const PlaylistRepository = getRepository(Playlist); 8 | const mixtapes = await PlaylistRepository.createQueryBuilder('playlist') 9 | .select([ 10 | 'playlist.id', 11 | 'playlist.title', 12 | 'playlist.subTitle', 13 | 'playlist.imageUrl', 14 | ]) 15 | .where('mixtape = 1') 16 | .getMany(); 17 | 18 | return res.json({ success: true, data: mixtapes }); 19 | } catch (err) { 20 | return res.status(500).json({ success: false }); 21 | } 22 | }; 23 | 24 | export { list }; 25 | -------------------------------------------------------------------------------- /server/src/routes/news/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as newsController from './news.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', newsController.list); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/src/routes/news/news.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { getRepository } from 'typeorm'; 3 | 4 | import News from '../../models/News'; 5 | 6 | const list = async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const NewsRepository = getRepository(News); 9 | const news = await NewsRepository.find(); 10 | return res.json({ 11 | success: true, 12 | data: [...news], 13 | }); 14 | } catch (err) { 15 | return res.status(500).json({ 16 | success: false, 17 | }); 18 | } 19 | }; 20 | 21 | export { list }; 22 | -------------------------------------------------------------------------------- /server/src/routes/playlists/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as playlistsContoller from './playlists.controller'; 3 | import { checkAuth } from '../../middlewares/auth'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', playlistsContoller.list); 8 | router.get('/:id', playlistsContoller.listById); 9 | router.use(checkAuth); 10 | router.post('/', playlistsContoller.create); 11 | router.post('/track', playlistsContoller.addTracks); 12 | router.post('/album', playlistsContoller.addAlbum); 13 | router.post('/mixtape', playlistsContoller.addPlaylist); 14 | router.post('/playlist', playlistsContoller.addPlaylist); 15 | export default router; 16 | -------------------------------------------------------------------------------- /server/src/routes/tracks/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as trackController from './tracks.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', trackController.list); 7 | router.get('/:id', trackController.findOne); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /server/src/routes/users/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as userController from './users.controller'; 3 | import { authenticateJWT } from '../../middlewares/auth'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', authenticateJWT, userController.getLoginedUser); 8 | router.post('/join', authenticateJWT, userController.joinUser); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6"], 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 6 | "moduleResolution": "node", 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 9 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 10 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 11 | "outDir": "./build", 12 | "sourceMap": true, 13 | "strictPropertyInitialization": false, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "downlevelIteration": true 17 | }, 18 | "exclude": ["node_modules/"] 19 | } 20 | --------------------------------------------------------------------------------