├── .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 |
--------------------------------------------------------------------------------